From 05bfc3651626b82845626820e23c73e4fd57b2ce Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH] Teampages page --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 14 ++- public/app/core/reducers/navModel.ts | 18 ++- public/app/core/selectors/location.ts | 3 + public/app/core/selectors/navModel.ts | 2 +- public/app/features/teams/TeamGroupSync.tsx | 12 +- public/app/features/teams/TeamMembers.tsx | 27 ++--- public/app/features/teams/TeamPages.test.tsx | 63 +++++++++++ public/app/features/teams/TeamPages.tsx | 107 +++++++++++------- public/app/features/teams/TeamSettings.tsx | 10 +- .../features/teams/__mocks__/navModelMock.ts | 59 ++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 87 ++++++++++++++ public/app/features/teams/state/actions.ts | 57 +++++++++- .../app/features/teams/state/reducers.test.ts | 6 +- public/app/features/teams/state/reducers.ts | 17 ++- public/app/features/teams/state/selectors.ts | 2 + public/app/types/index.ts | 7 +- 17 files changed, 410 insertions(+), 84 deletions(-) create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..b4b9b21126e 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { updateLocation } from './location'; +import { updateNavIndex } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..96465c6ef60 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,3 +1,9 @@ +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, }); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 26acdb39a3d..ac0e51854e7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -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; }; diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts new file mode 100644 index 00000000000..adc31f47e89 --- /dev/null +++ b/public/app/core/selectors/location.ts @@ -0,0 +1,3 @@ +export const getRouteParamsId = state => state.routeParams.id; + +export const getRouteParamsPage = state => state.routeParams.page; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index a7e1c3330bd..8b3a3edd84e 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,7 +1,7 @@ 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', diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..6562820d717 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,9 +1,8 @@ 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 { constructor(props) { super(props); @@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component { } componentDidMount() { - this.props.team.loadGroups(); + // this.props.team.loadGroups(); } renderGroup(group: TeamGroup) { @@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component { }; 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 { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.team.groups; return (
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index b06a547063a..32eb0d09b63 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,10 +1,9 @@ -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 { +export class TeamMembers extends PureComponent { 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 { }; 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 (
@@ -124,7 +121,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {team.members && team.members.map(member => this.renderMember(member))}
diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx new file mode 100644 index 00000000000..65084d0dc47 --- /dev/null +++ b/public/app/features/teams/TeamPages.test.tsx @@ -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(); + 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(); + }); +}); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2abc9c51535..606e254e7ec 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,77 +1,106 @@ -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 { +interface State { isSyncEnabled: boolean; - currentPage: string; +} +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { 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; + const { loadTeam, teamId } = 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')); + 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(); - if (!nav.main) { - return null; + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; } + return null; + } + + render() { + const { team, navModel } = this.props; + return (
- - {currentTeam && ( -
- {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
- )} + + {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
}
); } } -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)); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 0de60a0b16c..6e3c90d93f9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,30 +1,28 @@ 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 { 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() { diff --git a/public/app/features/teams/__mocks__/navModelMock.ts b/public/app/features/teams/__mocks__/navModelMock.ts new file mode 100644 index 00000000000..7aa8515ee13 --- /dev/null +++ b/public/app/features/teams/__mocks__/navModelMock.ts @@ -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', + }, + ], + }, + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap new file mode 100644 index 00000000000..3c19d726e41 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+`; + +exports[`Render should render group sync page 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render member page if team not empty 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render settings page 1`] = ` +
+ +
+ +
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5914a932ad0..35d07157dec 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,9 +1,12 @@ 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 = ThunkAction; +type ThunkResult = ThunkAction; 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 { }; } +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 { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index e115d311e37..0ab64a78e41 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,5 @@ 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'); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 673fd240668..56a2f83cd8d 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,9 +1,10 @@ -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, }; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 632bb2cd02a..40940cbae52 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,7 @@ export const getSearchQuery = state => state.searchQuery; +export const getTeam = state => state.team; + export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b867a8f6989..8b0010b6561 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -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;