From 306c3e6c10fe755e58ddfe622749f4bd0f2c11bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 12:34:32 +0200 Subject: [PATCH 01/11] creating types, actions, reducer --- .../teams}/TeamGroupSync.tsx | 0 .../Teams => features/teams}/TeamList.tsx | 17 ++++++++- .../Teams => features/teams}/TeamMembers.tsx | 0 .../Teams => features/teams}/TeamPages.tsx | 0 .../Teams => features/teams}/TeamSettings.tsx | 0 public/app/features/teams/state/actions.ts | 28 +++++++++++++++ public/app/features/teams/state/reducers.ts | 14 ++++++++ public/app/features/teams/state/selectors.ts | 1 + public/app/routes/routes.ts | 4 +-- public/app/types/index.ts | 36 +++++++++++++++++++ 10 files changed, 97 insertions(+), 3 deletions(-) rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamList.tsx (89%) rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamPages.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamSettings.tsx (100%) create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.ts diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 100% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 89% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index d0feee75184..79d71c33596 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,4 +1,5 @@ import React 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'; @@ -6,6 +7,8 @@ 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 { loadTeams } from './state/actions'; +import { getTeams } from './state/selectors'; interface Props { nav: typeof NavStore.Type; @@ -108,4 +111,16 @@ export class TeamList extends React.Component { } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + teams: getTeams(state), + }; +} + +function mapDispatchToProps() { + return { + loadTeams, + }; +} + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 100% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx similarity index 100% rename from public/app/containers/Teams/TeamPages.tsx rename to public/app/features/teams/TeamPages.tsx diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx similarity index 100% rename from public/app/containers/Teams/TeamSettings.tsx rename to public/app/features/teams/TeamSettings.tsx diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts new file mode 100644 index 00000000000..fafd2091217 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,28 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState, Team } from '../../../types'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export type Action = LoadTeamsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); + dispatch(teamsLoaded(teams)); + }; +} diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..a104ae2e21c --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,14 @@ +import { TeamsState } from '../../../types'; +import { Action } from './actions'; + +const initialState: TeamsState = { teams: [] }; + +export const teamsReducer = (state = initialState, action: Action): TeamsState => { + switch (action.type) { + } + return state; +}; + +export default { + teams: teamsReducer, +}; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..f1f66695e65 --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1 @@ +export const getTeams = state => state.teams; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index dfd215f7056..1fd1a474cd3 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..73cb05c26c1 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,9 @@ // Location // +import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; +import { types } from 'mobx-state-tree'; + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -53,6 +56,34 @@ export interface AlertRule { evalData?: { noData: boolean }; } +// +// Teams +// + +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; + search?: string; + members?: TeamMember[]; + groups?: TeamGroup[]; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + // // NavModel // @@ -89,8 +120,13 @@ export interface AlertRulesState { searchQuery: string; } +export interface TeamsState { + teams: Team[]; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; } From 167f0098193475a0ece4c280d26d1dfa4fee5d1a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:13:21 +0200 Subject: [PATCH 02/11] load teams and store in redux --- .../app/features/alerting/AlertRuleList.tsx | 4 +- public/app/features/teams/TeamList.test.tsx | 61 ++++++ public/app/features/teams/TeamList.tsx | 65 +++--- .../__snapshots__/TeamList.test.tsx.snap | 204 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 4 +- public/app/features/teams/state/reducers.ts | 4 +- public/app/stores/configureStore.ts | 2 + public/app/types/index.ts | 3 - 8 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 public/app/features/teams/TeamList.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4b48da47256..d30ba0ba802 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -115,7 +115,9 @@ export class AlertRuleList extends PureComponent {
    - {alertRules.map(rule => )} + {alertRules.map(rule => ( + {}} /> + ))}
diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..e7db3edfd3d --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamList, Props } from './TeamList'; +import { NavModel, Team } from '../../types'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + search: '', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + 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: [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ], + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => {}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 79d71c33596..801b66d24fc 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,44 +1,38 @@ -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 { NavModel, Team } from '../../types'; import { loadTeams } from './state/actions'; import { getTeams } 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[]; + loadTeams: typeof loadTeams; + search: string; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - constructor(props) { - super(props); - - this.props.nav.load('cfg', 'teams'); +export class TeamList extends PureComponent { + 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) => { + console.log('delete team', team); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + console.log('set search', event.target.value); + }; + + renderTeamMember(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -65,10 +59,11 @@ export class TeamList extends React.Component { } render() { - const { nav, teams } = this.props; + const { navModel, teams, search } = this.props; + return (
- +
@@ -77,7 +72,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -102,7 +97,7 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeamMember(team))}
@@ -113,14 +108,14 @@ export class TeamList extends React.Component { function mapStateToProps(state) { return { - teams: getTeams(state), + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + search: '', }; } -function mapDispatchToProps() { - return { - loadTeams, - }; -} +const mapDispatchToProps = { + loadTeams, +}; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap new file mode 100644 index 00000000000..c93dafde1c6 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + +
+ + Name + + Email + + Members + +
+
+
+
+`; + +exports[`Render should render teams table 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test + + + + test@test.com + + + + 1 + + + +
+
+
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index fafd2091217..35853bd73e8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -22,7 +22,7 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); - dispatch(teamsLoaded(teams)); + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a104ae2e21c..968c69d862c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,10 +1,12 @@ import { TeamsState } from '../../../types'; -import { Action } from './actions'; +import { Action, ActionTypes } from './actions'; const initialState: TeamsState = { teams: [] }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { + case ActionTypes.LoadTeams: + return { teams: action.payload }; } return state; }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 232f2e30cb8..a79c59a5fc1 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -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; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 73cb05c26c1..beb3253787a 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,9 +2,6 @@ // Location // -import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; -import { types } from 'mobx-state-tree'; - export interface LocationUpdate { path?: string; query?: UrlQueryMap; From 7e340b7aa5c1016c934a68d33597672e38e36584 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:32:51 +0200 Subject: [PATCH 03/11] delete team --- public/app/features/teams/TeamList.test.tsx | 38 +++++++++++++-------- public/app/features/teams/TeamList.tsx | 6 ++-- public/app/features/teams/state/actions.ts | 10 ++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index e7db3edfd3d..7e4caff80ae 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TeamList, Props } from './TeamList'; +import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; const setup = (propOverrides?: object) => { @@ -8,6 +8,7 @@ const setup = (propOverrides?: object) => { navModel: {} as NavModel, teams: [] as Team[], loadTeams: jest.fn(), + deleteTeam: jest.fn(), search: '', }; @@ -22,6 +23,17 @@ const setup = (propOverrides?: object) => { }; }; +const mockTeam: Team = { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], +}; + describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -30,18 +42,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ], + teams: [mockTeam], }); expect(wrapper).toMatchSnapshot(); @@ -58,4 +59,13 @@ describe('Life cycle', () => { }); }); -describe('Functions', () => {}); +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(mockTeam); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 801b66d24fc..df8776920d7 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams } from './state/actions'; +import { loadTeams, deleteTeam } from './state/actions'; import { getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -12,6 +12,7 @@ export interface Props { navModel: NavModel; teams: Team[]; loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; search: string; } @@ -25,7 +26,7 @@ export class TeamList extends PureComponent { } deleteTeam = (team: Team) => { - console.log('delete team', team); + this.props.deleteTeam(team.id); }; onSearchQueryChange = event => { @@ -116,6 +117,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadTeams, + deleteTeam, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35853bd73e8..6afed1828c1 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -26,3 +26,13 @@ export function loadTeams(): ThunkResult { dispatch(teamsLoaded(response.teams)); }; } + +export function deleteTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} From f68ac2021873bcc827de00423a5af90daab34faa Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 16:49:36 +0200 Subject: [PATCH 04/11] set search query action and tests --- public/app/features/teams/TeamList.test.tsx | 30 +-- public/app/features/teams/TeamList.tsx | 20 +- .../app/features/teams/__mocks__/teamMocks.ts | 32 +++ .../__snapshots__/TeamList.test.tsx.snap | 204 +++++++++++++++++- public/app/features/teams/state/actions.ts | 13 +- .../app/features/teams/state/reducers.test.ts | 41 ++++ public/app/features/teams/state/reducers.ts | 7 +- .../features/teams/state/selectors.test.ts | 25 +++ public/app/features/teams/state/selectors.ts | 10 +- public/app/types/index.ts | 1 + 10 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 7e4caff80ae..6c12f1357e5 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -2,6 +2,7 @@ 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 = { @@ -9,7 +10,8 @@ const setup = (propOverrides?: object) => { teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), - search: '', + setSearchQuery: jest.fn(), + searchQuery: '', }; Object.assign(props, propOverrides); @@ -23,17 +25,6 @@ const setup = (propOverrides?: object) => { }; }; -const mockTeam: Team = { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], -}; - describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -42,7 +33,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [mockTeam], + teams: getMultipleMockTeams(5), }); expect(wrapper).toMatchSnapshot(); @@ -63,9 +54,20 @@ describe('Functions', () => { describe('Delete team', () => { it('should call delete team', () => { const { instance } = setup(); - instance.deleteTeam(mockTeam); + 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'); + }); + }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index df8776920d7..a95f0f17a27 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,8 +4,8 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams, deleteTeam } from './state/actions'; -import { getTeams } from './state/selectors'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; export interface Props { @@ -13,7 +13,8 @@ export interface Props { teams: Team[]; loadTeams: typeof loadTeams; deleteTeam: typeof deleteTeam; - search: string; + setSearchQuery: typeof setSearchQuery; + searchQuery: string; } export class TeamList extends PureComponent { @@ -30,10 +31,10 @@ export class TeamList extends PureComponent { }; onSearchQueryChange = event => { - console.log('set search', event.target.value); + this.props.setSearchQuery(event.target.value); }; - renderTeamMember(team: Team) { + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -60,7 +61,7 @@ export class TeamList extends PureComponent { } render() { - const { navModel, teams, search } = this.props; + const { navModel, teams, searchQuery } = this.props; return (
@@ -73,7 +74,7 @@ export class TeamList extends PureComponent { type="text" className="gf-form-input" placeholder="Search teams" - value={search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -98,7 +99,7 @@ export class TeamList extends PureComponent { - {teams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
@@ -111,13 +112,14 @@ function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'teams'), teams: getTeams(state.teams), - search: '', + searchQuery: getSearchQuery(state.teams), }; } const mapDispatchToProps = { loadTeams, deleteTeam, + setSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..34405d2ce91 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,32 @@ +import { Team } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + let 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, + search: '', + members: [], + groups: [], + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index c93dafde1c6..6ea189f5dbd 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -167,7 +167,7 @@ exports[`Render should render teams table 1`] = ` - test + test-1 - test@test.com + test-1@test.com + + + + + + + + + test-2 + + + + + test-2@test.com + + + + + 2 + + + + + + + + + + + + + + + test-3 + + + + + test-3@test.com + + + + + 3 + + + + + + + + + + + + + + + test-4 + + + + + test-4@test.com + + + + + 4 + + + + + + + + + + + + + + + test-5 + + + + + test-5@test.com + + + + + 5 + + + + + +
diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 6afed1828c1..5914a932ad0 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -4,6 +4,7 @@ import { StoreState, Team } from '../../../types'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + SetSearchQuery = 'SET_SEARCH_QUERY', } export interface LoadTeamsAction { @@ -11,7 +12,12 @@ export interface LoadTeamsAction { payload: Team[]; } -export type Action = LoadTeamsAction; +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export type Action = LoadTeamsAction | SetSearchQueryAction; type ThunkResult = ThunkAction; @@ -20,6 +26,11 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ payload: teams, }); +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + export function loadTeams(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts new file mode 100644 index 00000000000..e115d311e37 --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,41 @@ +import { Action, ActionTypes } from './actions'; +import { initialState, teamsReducer } from './reducers'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialState, 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 968c69d862c..673fd240668 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,12 +1,15 @@ import { TeamsState } from '../../../types'; import { Action, ActionTypes } from './actions'; -const initialState: TeamsState = { teams: [] }; +export const initialState: TeamsState = { teams: [], searchQuery: '' }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: - return { teams: action.payload }; + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; } return state; }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts new file mode 100644 index 00000000000..66fd07444ce --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,25 @@ +import { getTeams } from './selectors'; +import { getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState } from '../../../types'; + +describe('Team 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); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index f1f66695e65..632bb2cd02a 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1 +1,9 @@ -export const getTeams = state => state.teams; +export const getSearchQuery = state => state.searchQuery; + +export const getTeams = state => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index beb3253787a..b867a8f6989 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -119,6 +119,7 @@ export interface AlertRulesState { export interface TeamsState { teams: Team[]; + searchQuery: string; } export interface StoreState { From 05bfc3651626b82845626820e23c73e4fd57b2ce Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH 05/11] 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; From 59b3bfd34293e46ef19c091e45f6be7d16fd309b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 18:01:59 +0200 Subject: [PATCH 06/11] team members, bug in fetching team --- public/app/features/teams/TeamMembers.tsx | 68 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 10 +++ public/app/features/teams/state/actions.ts | 67 +++++++++++++++++- .../app/features/teams/state/reducers.test.ts | 37 ++++++---- public/app/features/teams/state/reducers.ts | 8 ++- public/app/features/teams/state/selectors.ts | 8 ++- public/app/types/index.ts | 3 +- 8 files changed, 161 insertions(+), 44 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 32eb0d09b63..115fb40e184 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,12 +1,21 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; 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'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeam } from './state/selectors'; +import { getRouteParamsId } from '../../core/selectors/location'; interface Props { team: Team; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -21,20 +30,29 @@ export class TeamMembers extends PureComponent { } 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); + this.props.removeTeamMember(member.userId); } - removeMemberConfirmed(member: TeamMember) { - // this.props.team.removeMember(member); - } + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + this.props.addTeamMember(this.state.newTeamMember.id); + this.setState({ newTeamMember: null }); + }; renderMember(member: TeamMember) { return ( @@ -51,23 +69,9 @@ export class TeamMembers extends PureComponent { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onUserSelected = (user: User) => { - this.setState({ newTeamMember: user }); - }; - - onAddUserToTeam = async () => { - // 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 { team } = this.props; + const { team, searchMemberQuery } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -79,7 +83,7 @@ export class TeamMembers extends PureComponent { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -129,4 +133,20 @@ export class TeamMembers extends PureComponent { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 606e254e7ec..4395c0bfbef 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -63,7 +63,7 @@ export class TeamPages extends PureComponent { switch (currentPage) { case PageTypes.Members: - return ; + return ; case PageTypes.Settings: return ; @@ -95,7 +95,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), teamId: teamId, pageName: pageName, - team: getTeam(state.team), + team: getTeam(state.team, teamId), }; } diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 34405d2ce91..21c0cf012f0 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -30,3 +30,13 @@ export const getMockTeam = (): Team => { groups: [], }; }; + +export const getMockTeamMember = () => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35d07157dec..e407737bb20 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,6 +1,6 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; @@ -8,6 +8,8 @@ export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', } export interface LoadTeamsAction { @@ -20,12 +22,27 @@ export interface LoadTeamAction { payload: Team; } +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction; type ThunkResult = ThunkAction; @@ -39,6 +56,16 @@ const teamLoaded = (team: Team): LoadTeamAction => ({ payload: team, }); +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -89,6 +116,42 @@ export function loadTeam(id: number): ThunkResult { }; } +export function loadTeamMembers(): ThunkResult { + 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 { + 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 { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/members/${id}`) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + 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 0ab64a78e41..492ec71ba4b 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,20 +1,10 @@ import { Action, ActionTypes } from './actions'; -import { initialTeamsState, teamsReducer } from './reducers'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; describe('teams reducer', () => { it('should set teams', () => { - const payload = [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ]; + const payload = [getMockTeam()]; const action: Action = { type: ActionTypes.LoadTeams, @@ -39,3 +29,24 @@ describe('teams reducer', () => { expect(result.searchQuery).toEqual('test'); }); }); + +describe('team reducer', () => { + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + const mockTeam = getMockTeam(); + const state = { + ...initialTeamState, + team: mockTeam, + }; + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(state, action); + const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + + expect(result).toEqual(expectedState); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 56a2f83cd8d..e30fddb22a5 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -19,6 +19,12 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState switch (action.type) { case ActionTypes.LoadTeam: return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, team: { ...state.team, members: action.payload } }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 40940cbae52..d6142adf157 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,6 +1,12 @@ export const getSearchQuery = state => state.searchQuery; +export const getSearchMemberQuery = state => state.searchMemberQuery; -export const getTeam = state => state.team; +export const getTeam = (state, currentTeamId) => { + if (state.team.id === currentTeamId) { + console.log('yes'); + return 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 8b0010b6561..27ae3dbe19b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -124,7 +124,7 @@ export interface TeamsState { export interface TeamState { team: Team; - searchQuery: string; + searchMemberQuery: string; } export interface StoreState { @@ -132,4 +132,5 @@ export interface StoreState { location: LocationState; alertRules: AlertRulesState; teams: TeamsState; + team: TeamState; } From 9cfe2a7f5cce4fc0b7932c177ef2ba5351bbf030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 11:35:59 +0200 Subject: [PATCH 07/11] fix: Dashboard permissions now shows correctly, fixes #13201 --- public/app/features/dashboard/folder_picker/folder_picker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 352b29d27a0..80651fecb7e 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -131,6 +131,7 @@ export class FolderPickerCtrl { private loadInitialValue() { const resetFolder = { text: this.initialTitle, value: null }; const rootFolder = { text: this.rootName, value: 0 }; + this.getOptions('').then(result => { let folder; if (this.initialFolderId) { @@ -150,7 +151,7 @@ export class FolderPickerCtrl { this.folder = folder; // if this is not the same as our initial value notify parent - if (this.folder.id !== this.initialFolderId) { + if (this.folder.value !== this.initialFolderId) { this.onChange({ $folder: { id: this.folder.value, title: this.folder.text } }); } }); From d494ebc7309b363a8d131b1d719131864196e04c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:19:27 +0200 Subject: [PATCH 08/11] flattened team state, tests for TeamMembers --- .../app/features/teams/TeamMembers.test.tsx | 79 +++++ public/app/features/teams/TeamMembers.tsx | 24 +- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 26 +- .../__snapshots__/TeamMembers.test.tsx.snap | 317 ++++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 21 +- public/app/features/teams/state/actions.ts | 1 + .../app/features/teams/state/reducers.test.ts | 36 +- public/app/features/teams/state/reducers.ts | 11 +- .../features/teams/state/selectors.test.ts | 22 +- public/app/features/teams/state/selectors.ts | 11 +- public/app/types/index.ts | 5 +- 12 files changed, 493 insertions(+), 64 deletions(-) create mode 100644 public/app/features/teams/TeamMembers.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx new file mode 100644 index 00000000000..cae37e184fb --- /dev/null +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -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(); + 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); + }); +}); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 115fb40e184..5ad688aabf8 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,16 +1,14 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import { hot } from 'react-hot-loader'; 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'; +import { TeamMember } from '../../types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; -import { getSearchMemberQuery, getTeam } from './state/selectors'; -import { getRouteParamsId } from '../../core/selectors/location'; +import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + members: TeamMember[]; searchMemberQuery: string; loadTeamMembers: typeof loadTeamMembers; addTeamMember: typeof addTeamMember; @@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent { this.props.setSearchMemberQuery(event.target.value); }; - removeMember(member: TeamMember) { + onRemoveMember(member: TeamMember) { this.props.removeTeamMember(member.userId); } @@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - this.removeMember(member)} /> + this.onRemoveMember(member)} /> ); @@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent { render() { const { newTeamMember, isAdding } = this.state; - const { team, searchMemberQuery } = this.props; + const { searchMemberQuery, members } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent { - {team.members && team.members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member))}
@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent { } function mapStateToProps(state) { - const teamId = getRouteParamsId(state.location); - return { - team: getTeam(state.team, teamId), + members: getTeamMembers(state.team), searchMemberQuery: getSearchMemberQuery(state.team), }; } @@ -149,4 +145,4 @@ const mapDispatchToProps = { setSearchMemberQuery, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 4395c0bfbef..2528c3c87b8 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -41,10 +41,10 @@ export class TeamPages extends PureComponent { } componentDidMount() { - this.loadTeam(); + this.fetchTeam(); } - async loadTeam() { + async fetchTeam() { const { loadTeam, teamId } = this.props; await loadTeam(teamId); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 21c0cf012f0..7050997c387 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team } from '../../../types'; +import { Team, TeamMember } from '../../../types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { let teams: Team[] = []; @@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { avatarUrl: 'some/url/', email: `test-${i}@test.com`, memberCount: i, - search: '', - members: [], - groups: [], }); } @@ -25,13 +22,26 @@ export const getMockTeam = (): Team => { avatarUrl: 'some/url/', email: 'test@test.com', memberCount: 1, - search: '', - members: [], - groups: [], }; }; -export const getMockTeamMember = () => { +export const getMockTeamMembers = (amount: number): TeamMember[] => { + let 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, diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap new file mode 100644 index 00000000000..2a42897e2b9 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + +
+ + Name + + Email + +
+
+
+`; + +exports[`Render should render team members 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + +
+ + + testUser-1 + + test@test.com + + +
+ + + testUser-2 + + test@test.com + + +
+ + + testUser-3 + + test@test.com + + +
+ + + testUser-4 + + test@test.com + + +
+ + + testUser-5 + + test@test.com + + +
+
+
+`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 3c19d726e41..563d3d3bb99 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> @@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
- +
`; @@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e407737bb20..4786edf60a8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { + console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 492ec71ba4b..7f7a33d60ac 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -31,22 +31,42 @@ describe('teams reducer', () => { }); 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 mockTeam = getMockTeam(); - const state = { - ...initialTeamState, - team: mockTeam, - }; const action: Action = { type: ActionTypes.LoadTeamMembers, payload: [mockTeamMember], }; - const result = teamReducer(state, action); - const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + const result = teamReducer(initialTeamState, action); - expect(result).toEqual(expectedState); + 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'); }); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index e30fddb22a5..f02ade60923 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,8 +1,13 @@ -import { Team, TeamsState, TeamState } from '../../../types'; +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; +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) { @@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState return { ...state, team: action.payload }; case ActionTypes.LoadTeamMembers: - return { ...state, team: { ...state.team, members: action.payload } }; + return { ...state, members: action.payload }; case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 66fd07444ce..e1b11cf288b 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,8 +1,8 @@ -import { getTeams } from './selectors'; -import { getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState } from '../../../types'; +import { getTeam, getTeams } from './selectors'; +import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState, TeamState } from '../../../types'; -describe('Team selectors', () => { +describe('Teams selectors', () => { describe('Get teams', () => { const mockTeams = getMultipleMockTeams(5); @@ -23,3 +23,17 @@ describe('Team selectors', () => { }); }); }); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index d6142adf157..5e22f96eaf7 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; export const getTeam = (state, currentTeamId) => { - if (state.team.id === currentTeamId) { - console.log('yes'); + if (state.team.id === parseInt(currentTeamId)) { return state.team; } }; @@ -15,3 +14,11 @@ export const getTeams = state => { return regex.test(team.name); }); }; + +export const getTeamMembers = state => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 27ae3dbe19b..35cd9a41f4e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -63,9 +63,6 @@ export interface Team { avatarUrl: string; email: string; memberCount: number; - search?: string; - members?: TeamMember[]; - groups?: TeamGroup[]; } export interface TeamMember { @@ -124,6 +121,8 @@ export interface TeamsState { export interface TeamState { team: Team; + members: TeamMember[]; + groups: TeamGroup[]; searchMemberQuery: string; } From 841bd5817de3102c7310625aff94ed6d58d03bb2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:27:33 +0200 Subject: [PATCH 09/11] test for team member selector --- .../features/teams/state/selectors.test.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index e1b11cf288b..5f338069bbb 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,6 +1,6 @@ -import { getTeam, getTeams } from './selectors'; -import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState, TeamState } from '../../../types'; +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', () => { @@ -29,11 +29,28 @@ describe('Team selectors', () => { const mockTeam = getMockTeam(); it('should return team if matching with location team', () => { - const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + 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); + }); + }); }); From b1fe0c4c7e015b657fffe917638303950b2661c0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 15:53:58 +0200 Subject: [PATCH 10/11] team settings --- public/app/features/teams/TeamPages.tsx | 2 +- .../app/features/teams/TeamSettings.test.tsx | 44 ++++++++++++++ public/app/features/teams/TeamSettings.tsx | 59 ++++++++++++++----- .../__snapshots__/TeamPages.test.tsx.snap | 12 +--- .../__snapshots__/TeamSettings.test.tsx.snap | 57 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 14 +++++ 6 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2528c3c87b8..a4ab4a06d4d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -66,7 +66,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.Settings: - return ; + return ; case PageTypes.GroupSync: return isSyncEnabled && ; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx new file mode 100644 index 00000000000..2e40a0e3c44 --- /dev/null +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -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(); + 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'); + }); +}); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 6e3c90d93f9..ef9a5ae0b70 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,41 +1,58 @@ import React from 'react'; -import { hot } from 'react-hot-loader'; +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'; -interface Props { +export interface Props { team: Team; + updateTeam: typeof updateTeam; } -export class TeamSettings extends React.Component { +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { constructor(props) { super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; } - onChangeName = evt => { - // this.props.team.setName(evt.target.value); + onChangeName = event => { + this.setState({ name: event.target.value }); }; - onChangeEmail = evt => { - // this.props.team.setEmail(evt.target.value); + onChangeEmail = event => { + this.setState({ email: event.target.value }); }; - onUpdate = evt => { - evt.preventDefault(); - // this.props.team.update(); + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); }; render() { + const { name, email } = this.state; + return (

Team Settings

-
+
@@ -47,14 +64,14 @@ export class TeamSettings extends React.Component {
-
@@ -64,4 +81,16 @@ export class TeamSettings extends React.Component { } } -export default hot(module)(TeamSettings); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + updateTeam, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings); diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 563d3d3bb99..73f3fde4093 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -52,17 +52,7 @@ exports[`Render should render settings page 1`] = `
- +
`; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap new file mode 100644 index 00000000000..0f6573ccf90 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+

+ Team Settings +

+ +
+ + Name + + +
+
+ + Email + + +
+
+ +
+ +
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 4786edf60a8..5b203d0a502 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -153,6 +153,20 @@ export function removeTeamMember(id: number): ThunkResult { }; } +export function updateTeam(name: string, email: string): ThunkResult { + 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 deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() From 0cfcf2685e66af76895664c86f562952d63ca812 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 16:58:17 +0200 Subject: [PATCH 11/11] actions for group sync --- .../app/features/teams/TeamGroupSync.test.tsx | 0 public/app/features/teams/TeamGroupSync.tsx | 81 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 3 +- public/app/features/teams/state/actions.ts | 68 +++++++++++++++- public/app/features/teams/state/reducers.ts | 3 + public/app/features/teams/state/selectors.ts | 1 + 6 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 6562820d717..39fdd8d413e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,11 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; +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 { Team, TeamGroup } from '../../types'; +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 { @@ -15,14 +20,39 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -export class TeamGroupSync extends React.Component { +export class TeamGroupSync extends PureComponent { 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 = evt => { + this.setState({ newGroupId: evt.target.value }); + }; + + onAddGroup = () => { + 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) { @@ -38,30 +68,9 @@ export class TeamGroupSync extends React.Component { ); } - 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; + const groups = this.props.groups; return (
@@ -144,4 +153,16 @@ export class TeamGroupSync extends React.Component { } } -export default hot(module)(TeamGroupSync); +function mapStateToProps(state) { + return { + groups: getTeamGroups(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamGroups, + addTeamGroup, + removeTeamGroup, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index a4ab4a06d4d..f28bde518d2 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -57,7 +57,6 @@ export class TeamPages extends PureComponent { } renderPage() { - const { team } = this.props; const { isSyncEnabled } = this.state; const currentPage = this.getCurrentPage(); @@ -69,7 +68,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.GroupSync: - return isSyncEnabled && ; + return isSyncEnabled && ; } return null; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5b203d0a502..9b3ab3a8177 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,9 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; +import config from 'app/core/config'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -10,6 +11,7 @@ export enum ActionTypes { SetSearchQuery = 'SET_SEARCH_QUERY', SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', } export interface LoadTeamsAction { @@ -27,6 +29,11 @@ export interface LoadTeamMembersAction { payload: TeamMember[]; } +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; @@ -42,7 +49,8 @@ export type Action = | SetSearchQueryAction | LoadTeamAction | LoadTeamMembersAction - | SetSearchMemberQueryAction; + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; type ThunkResult = ThunkAction; @@ -61,6 +69,11 @@ const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => payload: teamMembers, }); +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ type: ActionTypes.SetSearchMemberQuery, payload: searchQuery, @@ -79,7 +92,7 @@ export function loadTeams(): ThunkResult { } function buildNavModel(team: Team): NavModelItem { - return { + const navModel = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', @@ -103,6 +116,18 @@ function buildNavModel(team: Team): NavModelItem { }, ], }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: 'team-settings', + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; } export function loadTeam(id: number): ThunkResult { @@ -117,7 +142,6 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { - console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; @@ -167,6 +191,42 @@ export function updateTeam(name: string, email: string): ThunkResult { }; } +export function loadTeamGroups(): ThunkResult { + 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 { + 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 { + 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 { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index f02ade60923..4af36f2e01c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -30,6 +30,9 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 5e22f96eaf7..416e293ec78 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,6 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; +export const getTeamGroups = state => state.groups; export const getTeam = (state, currentTeamId) => { if (state.team.id === parseInt(currentTeamId)) {