Peter Holmberg 7 лет назад
Родитель
Сommit
05bfc36516

+ 2 - 1
public/app/core/actions/index.ts

@@ -1,3 +1,4 @@
 import { updateLocation } from './location';
+import { updateNavIndex } from './navModel';
 
-export { updateLocation };
+export { updateLocation, updateNavIndex };

+ 11 - 3
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,
 });

+ 16 - 2
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;
 };

+ 3 - 0
public/app/core/selectors/location.ts

@@ -0,0 +1,3 @@
+export const getRouteParamsId = state => state.routeParams.id;
+
+export const getRouteParamsPage = state => state.routeParams.page;

+ 1 - 1
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',

+ 5 - 7
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<Props, State> {
   constructor(props) {
     super(props);
@@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
   }
 
   componentDidMount() {
-    this.props.team.loadGroups();
+    // this.props.team.loadGroups();
   }
 
   renderGroup(group: TeamGroup) {
@@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component<Props, State> {
   };
 
   onAddGroup = () => {
-    this.props.team.addGroup(this.state.newGroupId);
+    // this.props.team.addGroup(this.state.newGroupId);
     this.setState({ isAdding: false, newGroupId: '' });
   };
 
   onRemoveGroup = (group: TeamGroup) => {
-    this.props.team.removeGroup(group.groupId);
+    // this.props.team.removeGroup(group.groupId);
   };
 
   isNewGroupValid() {
@@ -63,7 +61,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
 
   render() {
     const { isAdding, newGroupId } = this.state;
-    const groups = this.props.team.groups.values();
+    const groups = this.props.team.groups;
 
     return (
       <div>

+ 12 - 15
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<Props, State> {
+export class TeamMembers extends PureComponent<Props, State> {
   constructor(props) {
     super(props);
     this.state = { isAdding: false, newTeamMember: null };
   }
 
   componentDidMount() {
-    this.props.team.loadMembers();
+    // this.props.team.loadMembers();
   }
 
   onSearchQueryChange = evt => {
-    this.props.team.setSearchQuery(evt.target.value);
+    // this.props.team.setSearchQuery(evt.target.value);
   };
 
   removeMember(member: TeamMember) {
-    this.props.team.removeMember(member);
+    // this.props.team.removeMember(member);
   }
 
   removeMemberConfirmed(member: TeamMember) {
-    this.props.team.removeMember(member);
+    // this.props.team.removeMember(member);
   }
 
   renderMember(member: TeamMember) {
@@ -62,16 +60,15 @@ export class TeamMembers extends React.Component<Props, State> {
   };
 
   onAddUserToTeam = async () => {
-    await this.props.team.addMember(this.state.newTeamMember.id);
-    await this.props.team.loadMembers();
-    this.setState({ newTeamMember: null });
+    // await this.props.team.addMember(this.state.newTeamMember.id);
+    // await this.props.team.loadMembers();
+    // this.setState({ newTeamMember: null });
   };
 
   render() {
     const { newTeamMember, isAdding } = this.state;
-    const members = this.props.team.filteredMembers;
-    const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
     const { team } = this.props;
+    const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
 
     return (
       <div>
@@ -124,7 +121,7 @@ export class TeamMembers extends React.Component<Props, State> {
                 <th style={{ width: '1%' }} />
               </tr>
             </thead>
-            <tbody>{members.map(member => this.renderMember(member))}</tbody>
+            <tbody>{team.members && team.members.map(member => this.renderMember(member))}</tbody>
           </table>
         </div>
       </div>

+ 63 - 0
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(<TeamPages {...props} />);
+  const instance = wrapper.instance();
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render member page if team not empty', () => {
+    const { wrapper } = setup({
+      team: getMockTeam(),
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render settings page', () => {
+    const { wrapper } = setup({
+      team: getMockTeam(),
+      pageName: 'settings',
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render group sync page', () => {
+    const { wrapper } = setup({
+      team: getMockTeam(),
+      pageName: 'groupsync',
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 68 - 39
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<Props, any> {
+interface State {
   isSyncEnabled: boolean;
-  currentPage: string;
+}
+
+enum PageTypes {
+  Members = 'members',
+  Settings = 'settings',
+  GroupSync = 'groupsync',
+}
 
+export class TeamPages extends PureComponent<Props, State> {
   constructor(props) {
     super(props);
 
-    this.isSyncEnabled = config.buildInfo.isEnterprise;
-    this.currentPage = this.getCurrentPage();
+    this.state = {
+      isSyncEnabled: config.buildInfo.isEnterprise,
+    };
+  }
 
+  componentDidMount() {
     this.loadTeam();
   }
 
   async loadTeam() {
-    const { teams, nav, view } = this.props;
-
-    await teams.loadById(view.routeParams.get('id'));
+    const { loadTeam, teamId } = this.props;
 
-    nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
-  }
-
-  getCurrentTeam(): Team {
-    const { teams, view } = this.props;
-    return teams.map.get(view.routeParams.get('id'));
+    await loadTeam(teamId);
   }
 
   getCurrentPage() {
     const pages = ['members', 'settings', 'groupsync'];
-    const currentPage = this.props.view.routeParams.get('page');
+    const currentPage = this.props.pageName;
     return _.includes(pages, currentPage) ? currentPage : pages[0];
   }
 
-  render() {
-    const { nav } = this.props;
-    const currentTeam = this.getCurrentTeam();
+  renderPage() {
+    const { team } = this.props;
+    const { isSyncEnabled } = this.state;
+    const currentPage = this.getCurrentPage();
+
+    switch (currentPage) {
+      case PageTypes.Members:
+        return <TeamMembers team={team} />;
 
-    if (!nav.main) {
-      return null;
+      case PageTypes.Settings:
+        return <TeamSettings team={team} />;
+
+      case PageTypes.GroupSync:
+        return isSyncEnabled && <TeamGroupSync team={team} />;
     }
 
+    return null;
+  }
+
+  render() {
+    const { team, navModel } = this.props;
+
     return (
       <div>
-        <PageHeader model={nav as any} />
-        {currentTeam && (
-          <div className="page-container page-body">
-            {this.currentPage === 'members' && <TeamMembers team={currentTeam} />}
-            {this.currentPage === 'settings' && <TeamSettings team={currentTeam} />}
-            {this.currentPage === 'groupsync' && this.isSyncEnabled && <TeamGroupSync team={currentTeam} />}
-          </div>
-        )}
+        <PageHeader model={navModel} />
+        {team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
       </div>
     );
   }
 }
 
-export default hot(module)(TeamPages);
+function mapStateToProps(state) {
+  const teamId = getRouteParamsId(state.location);
+  const pageName = getRouteParamsPage(state.location) || 'members';
+
+  return {
+    navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`),
+    teamId: teamId,
+    pageName: pageName,
+    team: getTeam(state.team),
+  };
+}
+
+const mapDispatchToProps = {
+  loadTeam,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));

+ 4 - 6
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<Props, any> {
   constructor(props) {
     super(props);
   }
 
   onChangeName = evt => {
-    this.props.team.setName(evt.target.value);
+    // this.props.team.setName(evt.target.value);
   };
 
   onChangeEmail = evt => {
-    this.props.team.setEmail(evt.target.value);
+    // this.props.team.setEmail(evt.target.value);
   };
 
   onUpdate = evt => {
     evt.preventDefault();
-    this.props.team.update();
+    // this.props.team.update();
   };
 
   render() {

+ 59 - 0
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',
+        },
+      ],
+    },
+  };
+};

+ 87 - 0
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`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+</div>
+`;
+
+exports[`Render should render group sync page 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <TeamGroupSync
+      team={
+        Object {
+          "avatarUrl": "some/url/",
+          "email": "test@test.com",
+          "groups": Array [],
+          "id": 1,
+          "memberCount": 1,
+          "members": Array [],
+          "name": "test",
+          "search": "",
+        }
+      }
+    />
+  </div>
+</div>
+`;
+
+exports[`Render should render member page if team not empty 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <TeamMembers
+      team={
+        Object {
+          "avatarUrl": "some/url/",
+          "email": "test@test.com",
+          "groups": Array [],
+          "id": 1,
+          "memberCount": 1,
+          "members": Array [],
+          "name": "test",
+          "search": "",
+        }
+      }
+    />
+  </div>
+</div>
+`;
+
+exports[`Render should render settings page 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <TeamSettings
+      team={
+        Object {
+          "avatarUrl": "some/url/",
+          "email": "test@test.com",
+          "groups": Array [],
+          "id": 1,
+          "memberCount": 1,
+          "members": Array [],
+          "name": "test",
+          "search": "",
+        }
+      }
+    />
+  </div>
+</div>
+`;

+ 54 - 3
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<R> = ThunkAction<R, StoreState, undefined, Action>;
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
 
 const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({
   type: ActionTypes.LoadTeams,
   payload: teams,
 });
 
+const teamLoaded = (team: Team): LoadTeamAction => ({
+  type: ActionTypes.LoadTeam,
+  payload: team,
+});
+
 export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
   type: ActionTypes.SetSearchQuery,
   payload: searchQuery,
@@ -38,6 +51,44 @@ export function loadTeams(): ThunkResult<void> {
   };
 }
 
+function buildNavModel(team: Team): NavModelItem {
+  return {
+    img: team.avatarUrl,
+    id: 'team-' + team.id,
+    subTitle: 'Manage members & settings',
+    url: '',
+    text: team.name,
+    breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
+    children: [
+      {
+        active: false,
+        icon: 'gicon gicon-team',
+        id: `team-members-${team.id}`,
+        text: 'Members',
+        url: `org/teams/edit/${team.id}/members`,
+      },
+      {
+        active: false,
+        icon: 'fa fa-fw fa-sliders',
+        id: `team-settings-${team.id}`,
+        text: 'Settings',
+        url: `org/teams/edit/${team.id}/settings`,
+      },
+    ],
+  };
+}
+
+export function loadTeam(id: number): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv()
+      .get(`/api/teams/${id}`)
+      .then(response => {
+        dispatch(teamLoaded(response));
+        dispatch(updateNavIndex(buildNavModel(response)));
+      });
+  };
+}
+
 export function deleteTeam(id: number): ThunkResult<void> {
   return async dispatch => {
     await getBackendSrv()

+ 3 - 3
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');
   });

+ 14 - 3
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,
 };

+ 2 - 0
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');
 

+ 6 - 1
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;