From 9f73f13091d5ebaf4ba4e7ad40d7bdb9556e88d1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 11 Sep 2018 14:14:03 +0200 Subject: [PATCH] Teams page replace mobx (#13219) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * team settings * actions for group sync * tests for team groups * removed comment * remove old stores * fix: formating of datasource.go * fix: minor changes to imports * adding debounce and fixing issue in teamlist * refactoring: moving types to their own files --- public/app/containers/Teams/TeamPages.tsx | 77 ---- public/app/containers/Teams/TeamSettings.tsx | 69 ---- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 22 +- .../CustomScrollbar/CustomScrollbar.tsx | 1 - public/app/core/reducers/navModel.ts | 18 +- public/app/core/selectors/location.ts | 3 + public/app/features/alerting/state/actions.ts | 8 +- .../features/alerting/state/reducers.test.ts | 4 +- .../app/features/alerting/state/reducers.ts | 4 +- .../app/features/teams/TeamGroupSync.test.tsx | 63 ++++ .../teams}/TeamGroupSync.tsx | 95 +++-- public/app/features/teams/TeamList.test.tsx | 75 ++++ .../Teams => features/teams}/TeamList.tsx | 126 ++++--- .../app/features/teams/TeamMembers.test.tsx | 79 ++++ .../Teams => features/teams}/TeamMembers.tsx | 91 +++-- public/app/features/teams/TeamPages.test.tsx | 63 ++++ public/app/features/teams/TeamPages.tsx | 105 ++++++ .../app/features/teams/TeamSettings.test.tsx | 44 +++ public/app/features/teams/TeamSettings.tsx | 96 +++++ .../features/teams/__mocks__/navModelMock.ts | 59 +++ .../app/features/teams/__mocks__/teamMocks.ts | 65 ++++ .../__snapshots__/TeamGroupSync.test.tsx.snap | 281 ++++++++++++++ .../__snapshots__/TeamList.test.tsx.snap | 354 ++++++++++++++++++ .../__snapshots__/TeamMembers.test.tsx.snap | 317 ++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 48 +++ .../__snapshots__/TeamSettings.test.tsx.snap | 57 +++ public/app/features/teams/state/actions.ts | 237 ++++++++++++ .../app/features/teams/state/reducers.test.ts | 72 ++++ public/app/features/teams/state/reducers.ts | 44 +++ .../features/teams/state/selectors.test.ts | 56 +++ public/app/features/teams/state/selectors.ts | 30 ++ public/app/routes/routes.ts | 4 +- public/app/stores/NavStore/NavStore.ts | 40 -- public/app/stores/RootStore/RootStore.ts | 4 - public/app/stores/TeamsStore/TeamsStore.ts | 156 -------- public/app/stores/configureStore.ts | 2 + public/app/types/alerting.ts | 35 ++ public/app/types/index.ts | 112 ++---- public/app/types/location.ts | 15 + public/app/types/navModel.ts | 22 ++ public/app/types/teams.ts | 32 ++ 42 files changed, 2493 insertions(+), 595 deletions(-) delete mode 100644 public/app/containers/Teams/TeamPages.tsx delete mode 100644 public/app/containers/Teams/TeamSettings.tsx create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (73%) create mode 100644 public/app/features/teams/TeamList.test.tsx rename public/app/{containers/Teams => features/teams}/TeamList.tsx (63%) create mode 100644 public/app/features/teams/TeamMembers.test.tsx rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (66%) create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/TeamPages.tsx create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/TeamSettings.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.test.ts create mode 100644 public/app/features/teams/state/selectors.ts delete mode 100644 public/app/stores/TeamsStore/TeamsStore.ts create mode 100644 public/app/types/alerting.ts create mode 100644 public/app/types/location.ts create mode 100644 public/app/types/navModel.ts create mode 100644 public/app/types/teams.ts diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx deleted file mode 100644 index 2abc9c51535..00000000000 --- a/public/app/containers/Teams/TeamPages.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import config from 'app/core/config'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { ViewStore } from 'app/stores/ViewStore/ViewStore'; -import TeamMembers from './TeamMembers'; -import TeamSettings from './TeamSettings'; -import TeamGroupSync from './TeamGroupSync'; - -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - view: typeof ViewStore.Type; -} - -@inject('nav', 'teams', 'view') -@observer -export class TeamPages extends React.Component { - isSyncEnabled: boolean; - currentPage: string; - - constructor(props) { - super(props); - - this.isSyncEnabled = config.buildInfo.isEnterprise; - this.currentPage = this.getCurrentPage(); - - this.loadTeam(); - } - - async loadTeam() { - const { teams, nav, view } = this.props; - - await teams.loadById(view.routeParams.get('id')); - - nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); - } - - getCurrentTeam(): Team { - const { teams, view } = this.props; - return teams.map.get(view.routeParams.get('id')); - } - - getCurrentPage() { - const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.view.routeParams.get('page'); - return _.includes(pages, currentPage) ? currentPage : pages[0]; - } - - render() { - const { nav } = this.props; - const currentTeam = this.getCurrentTeam(); - - if (!nav.main) { - return null; - } - - return ( -
- - {currentTeam && ( -
- {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
- )} -
- ); - } -} - -export default hot(module)(TeamPages); diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx deleted file mode 100644 index 0de60a0b16c..00000000000 --- a/public/app/containers/Teams/TeamSettings.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team } from 'app/stores/TeamsStore/TeamsStore'; -import { Label } from 'app/core/components/Forms/Forms'; - -interface Props { - team: Team; -} - -@observer -export class TeamSettings extends React.Component { - constructor(props) { - super(props); - } - - onChangeName = evt => { - this.props.team.setName(evt.target.value); - }; - - onChangeEmail = evt => { - this.props.team.setEmail(evt.target.value); - }; - - onUpdate = evt => { - evt.preventDefault(); - this.props.team.update(); - }; - - render() { - return ( -
-

Team Settings

-
-
- - -
-
- - -
- -
- -
-
-
- ); - } -} - -export default hot(module)(TeamSettings); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..451a13dae99 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, UpdateNavIndexAction } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..a40a0e880ee 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,13 +1,17 @@ -export type Action = UpdateNavIndexAction; +import { NavModelItem } from '../../types'; -// this action is not used yet -// kind of just a placeholder, will be need for dynamic pages -// like datasource edit, teams edit page - -export interface UpdateNavIndexAction { - type: 'UPDATE_NAV_INDEX'; +export enum ActionTypes { + UpdateNavIndex = 'UPDATE_NAV_INDEX', } -export const updateNavIndex = (): UpdateNavIndexAction => ({ - type: 'UPDATE_NAV_INDEX', +export type Action = UpdateNavIndexAction; + +export interface UpdateNavIndexAction { + type: ActionTypes.UpdateNavIndex; + payload: NavModelItem; +} + +export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({ + type: ActionTypes.UpdateNavIndex, + payload: item, }); diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx index 8be65249808..9b9a9c4d02a 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx @@ -13,7 +13,6 @@ interface Props { * Wraps component into component from `react-custom-scrollbars` */ class CustomScrollbar extends PureComponent { - static defaultProps: Partial = { customClassName: 'custom-scrollbars', autoHide: true, 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/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index ca50d9e1038..edd6fbb1da1 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,15 +1,15 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AlertRuleApi, StoreState } from 'app/types'; +import { AlertRuleDTO, StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; export enum ActionTypes { LoadAlertRules = 'LOAD_ALERT_RULES', - SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchQuery = 'SET_ALERT_SEARCH_QUERY', } export interface LoadAlertRulesAction { type: ActionTypes.LoadAlertRules; - payload: AlertRuleApi[]; + payload: AlertRuleDTO[]; } export interface SetSearchQueryAction { @@ -17,7 +17,7 @@ export interface SetSearchQueryAction { payload: string; } -export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({ +export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({ type: ActionTypes.LoadAlertRules, payload: rules, }); diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts index 96ca7bacf6c..4f079a090cf 100644 --- a/public/app/features/alerting/state/reducers.test.ts +++ b/public/app/features/alerting/state/reducers.test.ts @@ -1,9 +1,9 @@ import { ActionTypes, Action } from './actions'; import { alertRulesReducer, initialState } from './reducers'; -import { AlertRuleApi } from '../../../types'; +import { AlertRuleDTO } from 'app/types'; describe('Alert rules', () => { - const payload: AlertRuleApi[] = [ + const payload: AlertRuleDTO[] = [ { id: 2, dashboardId: 7, diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 73feb3cb260..c525885bc9c 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types'; +import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types'; import { Action, ActionTypes } from './actions'; import alertDef from './alertDef'; @@ -29,7 +29,7 @@ function convertToAlertRule(rule, state): AlertRule { export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => { switch (action.type) { case ActionTypes.LoadAlertRules: { - const alertRules: AlertRuleApi[] = action.payload; + const alertRules: AlertRuleDTO[] = action.payload; const alertRulesViewModel: AlertRule[] = alertRules.map(rule => { return convertToAlertRule(rule, rule.state); diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..f3deb62c77b --- /dev/null +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamGroupSync } from './TeamGroupSync'; +import { TeamGroup } from '../../types'; +import { getMockTeamGroups } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + groups: [] as TeamGroup[], + loadTeamGroups: jest.fn(), + addTeamGroup: jest.fn(), + removeTeamGroup: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamGroupSync; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render groups table', () => { + const { wrapper } = setup({ + groups: getMockTeamGroups(3), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should call add group', () => { + const { instance } = setup(); + + instance.setState({ newGroupId: 'some/group' }); + const mockEvent = { preventDefault: jest.fn() }; + + instance.onAddGroup(mockEvent); + + expect(instance.props.addTeamGroup).toHaveBeenCalledWith('some/group'); + }); + + it('should call remove group', () => { + const { instance } = setup(); + + const mockGroup: TeamGroup = { teamId: 1, groupId: 'some/group' }; + + instance.onRemoveGroup(mockGroup); + + expect(instance.props.removeTeamGroup).toHaveBeenCalledWith('some/group'); + }); +}); diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 73% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..939dfcc8e31 100644 --- a/public/app/containers/Teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,12 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { TeamGroup } from '../../types'; +import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; +import { getTeamGroups } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + groups: TeamGroup[]; + loadTeamGroups: typeof loadTeamGroups; + addTeamGroup: typeof addTeamGroup; + removeTeamGroup: typeof removeTeamGroup; } interface State { @@ -16,15 +20,40 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -@observer -export class TeamGroupSync extends React.Component { +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 = event => { + this.setState({ newGroupId: event.target.value }); + }; + + onAddGroup = event => { + event.preventDefault(); + this.props.addTeamGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: TeamGroup) => { + this.props.removeTeamGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; } renderGroup(group: TeamGroup) { @@ -40,30 +69,9 @@ export class TeamGroupSync extends React.Component { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onNewGroupIdChanged = evt => { - this.setState({ newGroupId: evt.target.value }); - }; - - onAddGroup = () => { - this.props.team.addGroup(this.state.newGroupId); - this.setState({ isAdding: false, newGroupId: '' }); - }; - - onRemoveGroup = (group: TeamGroup) => { - this.props.team.removeGroup(group.groupId); - }; - - isNewGroupValid() { - return this.state.newGroupId.length > 1; - } - render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.groups; return (
@@ -86,7 +94,7 @@ export class TeamGroupSync extends React.Component {
Add External Group
-
+
{
-
-
+
@@ -146,4 +149,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/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..6f84ca920d0 --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamList } from './TeamList'; +import { NavModel, Team } from '../../types'; +import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + deleteTeam: jest.fn(), + setSearchQuery: jest.fn(), + searchQuery: '', + teamsCount: 0, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamList; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render teams table', () => { + const { wrapper } = setup({ + teams: getMultipleMockTeams(5), + teamsCount: 5, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(getMockTeam()); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); + + describe('on search query change', () => { + it('should call setSearchQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'test' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test'); + }); + }); +}); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 63% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index 2a5743bea96..985d73d9a52 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,42 +1,42 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { NavModel, Team } from '../../types'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; +import { getNavModel } from 'app/core/selectors/navModel'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - backendSrv: BackendSrv; +export interface Props { + navModel: NavModel; + teams: Team[]; + searchQuery: string; + teamsCount: number; + loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; + setSearchQuery: typeof setSearchQuery; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - 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) => { + this.props.deleteTeam(team.id); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + this.props.setSearchQuery(event.target.value); + }; + + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -62,7 +62,28 @@ export class TeamList extends React.Component { ); } - renderTeamList(teams) { + renderEmptyList() { + return ( +
+ +
+ ); + } + + renderTeamList() { + const { teams, searchQuery } = this.props; + return (
@@ -72,7 +93,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -97,49 +118,38 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
); } - renderEmptyList() { - return ( -
- -
- ); - } - render() { - const { nav, teams } = this.props; - let view; - - if (teams.filteredTeams.length > 0) { - view = this.renderTeamList(teams); - } else { - view = this.renderEmptyList(); - } + const { navModel, teamsCount } = this.props; return (
- - {view} + + {teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
); } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + searchQuery: getSearchQuery(state.teams), + teamsCount: getTeamsCount(state.teams), + }; +} + +const mapDispatchToProps = { + loadTeams, + deleteTeam, + setSearchQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); 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/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 66% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx index b06a547063a..5ad688aabf8 100644 --- a/public/app/containers/Teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,13 +1,19 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { TeamMember } from '../../types'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + members: TeamMember[]; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -15,42 +21,22 @@ interface State { newTeamMember?: User; } -@observer -export class TeamMembers extends React.Component { +export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; } componentDidMount() { - this.props.team.loadMembers(); + this.props.loadTeamMembers(); } - onSearchQueryChange = evt => { - this.props.team.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + this.props.setSearchMemberQuery(event.target.value); }; - removeMember(member: TeamMember) { - this.props.team.removeMember(member); - } - - removeMemberConfirmed(member: TeamMember) { - this.props.team.removeMember(member); - } - - renderMember(member: TeamMember) { - return ( - - - - - {member.login} - {member.email} - - this.removeMember(member)} /> - - - ); + onRemoveMember(member: TeamMember) { + this.props.removeTeamMember(member.userId); } onToggleAdding = () => { @@ -62,16 +48,29 @@ export class TeamMembers extends React.Component { }; onAddUserToTeam = async () => { - await this.props.team.addMember(this.state.newTeamMember.id); - await this.props.team.loadMembers(); + this.props.addTeamMember(this.state.newTeamMember.id); this.setState({ newTeamMember: null }); }; + renderMember(member: TeamMember) { + return ( + + + + + {member.login} + {member.email} + + this.onRemoveMember(member)} /> + + + ); + } + render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.filteredMembers; + const { searchMemberQuery, members } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); - const { team } = this.props; return (
@@ -82,7 +81,7 @@ export class TeamMembers extends React.Component { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -124,7 +123,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member))}
@@ -132,4 +131,18 @@ export class TeamMembers extends React.Component { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + return { + members: getTeamMembers(state.team), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); 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 new file mode 100644 index 00000000000..f28bde518d2 --- /dev/null +++ b/public/app/features/teams/TeamPages.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import _ from 'lodash'; +import { hot } from 'react-hot-loader'; +import config from 'app/core/config'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import TeamMembers from './TeamMembers'; +import TeamSettings from './TeamSettings'; +import TeamGroupSync from './TeamGroupSync'; +import { NavModel, Team } from '../../types'; +import { loadTeam } from './state/actions'; +import { getTeam } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; + +export interface Props { + team: Team; + loadTeam: typeof loadTeam; + teamId: number; + pageName: string; + navModel: NavModel; +} + +interface State { + isSyncEnabled: boolean; +} + +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isSyncEnabled: config.buildInfo.isEnterprise, + }; + } + + componentDidMount() { + this.fetchTeam(); + } + + async fetchTeam() { + const { loadTeam, teamId } = this.props; + + await loadTeam(teamId); + } + + getCurrentPage() { + const pages = ['members', 'settings', 'groupsync']; + const currentPage = this.props.pageName; + return _.includes(pages, currentPage) ? currentPage : pages[0]; + } + + renderPage() { + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); + + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; + } + + return null; + } + + render() { + const { team, navModel } = this.props; + + return ( +
+ + {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
} +
+ ); + } +} + +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + const pageName = getRouteParamsPage(state.location) || 'members'; + + return { + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + teamId: teamId, + pageName: pageName, + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + loadTeam, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages)); 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 new file mode 100644 index 00000000000..ef9a5ae0b70 --- /dev/null +++ b/public/app/features/teams/TeamSettings.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Label } from 'app/core/components/Forms/Forms'; +import { Team } from '../../types'; +import { updateTeam } from './state/actions'; +import { getRouteParamsId } from '../../core/selectors/location'; +import { getTeam } from './state/selectors'; + +export interface Props { + team: Team; + updateTeam: typeof updateTeam; +} + +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { + constructor(props) { + super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; + } + + onChangeName = event => { + this.setState({ name: event.target.value }); + }; + + onChangeEmail = event => { + this.setState({ email: event.target.value }); + }; + + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); + }; + + render() { + const { name, email } = this.state; + + return ( +
+

Team Settings

+
+
+ + +
+
+ + +
+ +
+ +
+
+
+ ); + } +} + +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/__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/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..c9e9a27bee0 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,65 @@ +import { Team, TeamGroup, TeamMember } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + const teams: Team[] = []; + for (let i = 1; i <= numberOfTeams; i++) { + teams.push({ + id: i, + name: `test-${i}`, + avatarUrl: 'some/url/', + email: `test-${i}@test.com`, + memberCount: i, + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + }; +}; + +export const getMockTeamMembers = (amount: number): TeamMember[] => { + const teamMembers: TeamMember[] = []; + + for (let i = 1; i <= amount; i++) { + teamMembers.push({ + userId: i, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: `testUser-${i}`, + }); + } + + return teamMembers; +}; + +export const getMockTeamMember = (): TeamMember => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; + +export const getMockTeamGroups = (amount: number): TeamGroup[] => { + const groups: TeamGroup[] = []; + + for (let i = 1; i <= amount; i++) { + groups.push({ + groupId: `group-${i}`, + teamId: 1, + }); + } + + return groups; +}; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap new file mode 100644 index 00000000000..5a143f19038 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -0,0 +1,281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+
+

+ External group sync +

+ + + +
+
+ +
+ +
+ Add External Group +
+
+
+ +
+
+ +
+
+
+
+
+
+ There are no external groups to sync with +
+ +
+ + + Sync LDAP or OAuth groups with your Grafana teams. + + Learn more + +
+
+
+`; + +exports[`Render should render groups table 1`] = ` +
+
+

+ External group sync +

+ + + +
+ +
+ +
+ +
+ Add External Group +
+
+
+ +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ External Group ID + +
+ group-1 + + + + +
+ group-2 + + + + +
+ group-3 + + + + +
+
+
+`; 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..45d0f78126e --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render teams table 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + +
+ + + + + + 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/__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 new file mode 100644 index 00000000000..4ce4df4acb2 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,48 @@ +// 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/__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 new file mode 100644 index 00000000000..91aa899e171 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,237 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; +import config from 'app/core/config'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', + LoadTeam = 'LOAD_TEAM', + SetSearchQuery = 'SET_TEAM_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export interface LoadTeamAction { + type: ActionTypes.LoadTeam; + payload: Team; +} + +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +const teamLoaded = (team: Team): LoadTeamAction => ({ + type: ActionTypes.LoadTeam, + payload: team, +}); + +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); + }; +} + +function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function loadTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + +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 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 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() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} 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..7f7a33d60ac --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,72 @@ +import { Action, ActionTypes } from './actions'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [getMockTeam()]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.searchQuery).toEqual('test'); + }); +}); + +describe('team reducer', () => { + it('should set team', () => { + const payload = getMockTeam(); + + const action: Action = { + type: ActionTypes.LoadTeam, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.team).toEqual(payload); + }); + + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.members).toEqual([mockTeamMember]); + }); + + it('should set member search query', () => { + const payload = 'member'; + + const action: Action = { + type: ActionTypes.SetSearchMemberQuery, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.searchMemberQuery).toEqual('member'); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..8b76028b9cb --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,44 @@ +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamState: TeamState = { + team: {} as Team, + members: [] as TeamMember[], + groups: [] as TeamGroup[], + searchMemberQuery: '', +}; + +export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { + switch (action.type) { + case ActionTypes.LoadTeams: + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; + } + return state; +}; + +export const teamReducer = (state = initialTeamState, action: Action): TeamState => { + switch (action.type) { + case ActionTypes.LoadTeam: + return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, members: action.payload }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; + } + + return state; +}; + +export default { + teams: teamsReducer, + team: teamReducer, +}; 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..5f338069bbb --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,56 @@ +import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; + +describe('Teams selectors', () => { + describe('Get teams', () => { + const mockTeams = getMultipleMockTeams(5); + + it('should return teams if no search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '' }; + + const teams = getTeams(mockState); + + expect(teams).toEqual(mockTeams); + }); + + it('Should filter teams if search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' }; + + const teams = getTeams(mockState); + + expect(teams.length).toEqual(1); + }); + }); +}); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); + + describe('Get members', () => { + const mockTeamMembers = getMockTeamMembers(5); + + it('should return team members', () => { + const mockState: TeamState = { + team: {} as Team, + searchMemberQuery: '', + members: mockTeamMembers, + groups: [] as TeamGroup[], + }; + + const members = getTeamMembers(mockState); + + expect(members).toEqual(mockTeamMembers); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..9201993bf0d --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1,30 @@ +import { Team, TeamsState, TeamState } from 'app/types'; + +export const getSearchQuery = (state: TeamsState) => state.searchQuery; +export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery; +export const getTeamGroups = (state: TeamState) => state.groups; +export const getTeamsCount = (state: TeamsState) => state.teams.length; + +export const getTeam = (state: TeamState, currentTeamId): Team | null => { + if (state.team.id === parseInt(currentTeamId, 10)) { + return state.team; + } + + return null; +}; + +export const getTeams = (state: TeamsState) => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; + +export const getTeamMembers = (state: TeamState) => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a0b070cbcb4..519008d70f5 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,9 +4,9 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index d869b0f740d..f87cc486b41 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; import { types, getEnv } from 'mobx-state-tree'; import { NavItem } from './NavItem'; -import { Team } from '../TeamsStore/TeamsStore'; export const NavStore = types .model('NavStore', { @@ -116,43 +115,4 @@ export const NavStore = types self.main = NavItem.create(main); }, - - initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) { - const main = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: tab === 'members', - icon: 'gicon gicon-team', - id: 'team-members', - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: tab === 'settings', - icon: 'fa fa-fw fa-sliders', - id: 'team-settings', - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (isSyncEnabled) { - main.children.splice(1, 0, { - active: tab === 'groupsync', - icon: 'fa fa-fw fa-refresh', - id: 'team-settings', - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - self.main = NavItem.create(main); - }, })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index fba25e5f015..37c13f48c61 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -3,7 +3,6 @@ import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; -import { TeamsStore } from './../TeamsStore/TeamsStore'; export const RootStore = types.model({ nav: types.optional(NavStore, {}), @@ -17,9 +16,6 @@ export const RootStore = types.model({ routeParams: {}, }), folder: types.optional(FolderStore, {}), - teams: types.optional(TeamsStore, { - map: {}, - }), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts deleted file mode 100644 index bc8af218def..00000000000 --- a/public/app/stores/TeamsStore/TeamsStore.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const TeamMemberModel = types.model('TeamMember', { - userId: types.identifier(types.number), - teamId: types.number, - avatarUrl: types.string, - email: types.string, - login: types.string, -}); - -type TeamMemberType = typeof TeamMemberModel.Type; -export interface TeamMember extends TeamMemberType {} - -export const TeamGroupModel = types.model('TeamGroup', { - groupId: types.identifier(types.string), - teamId: types.number, -}); - -type TeamGroupType = typeof TeamGroupModel.Type; -export interface TeamGroup extends TeamGroupType {} - -export const TeamModel = types - .model('Team', { - id: types.identifier(types.number), - name: types.string, - avatarUrl: types.string, - email: types.string, - memberCount: types.number, - search: types.optional(types.string, ''), - members: types.optional(types.map(TeamMemberModel), {}), - groups: types.optional(types.map(TeamGroupModel), {}), - }) - .views(self => ({ - get filteredMembers(this: Team) { - const members = this.members.values(); - const regex = new RegExp(self.search, 'i'); - return members.filter(member => { - return regex.test(member.login) || regex.test(member.email); - }); - }, - })) - .actions(self => ({ - setName(name: string) { - self.name = name; - }, - - setEmail(email: string) { - self.email = email; - }, - - setSearchQuery(query: string) { - self.search = query; - }, - - update: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - - yield backendSrv.put(`/api/teams/${self.id}`, { - name: self.name, - email: self.email, - }); - }), - - loadMembers: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`); - self.members.clear(); - - for (const member of rsp) { - self.members.set(member.userId.toString(), TeamMemberModel.create(member)); - } - }), - - removeMember: flow(function* load(member: TeamMember) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`); - // remove from store map - self.members.delete(member.userId.toString()); - }), - - addMember: flow(function* load(userId: number) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId }); - }), - - loadGroups: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`); - self.groups.clear(); - - for (const group of rsp) { - self.groups.set(group.groupId, TeamGroupModel.create(group)); - } - }), - - addGroup: flow(function* load(groupId: string) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId }); - self.groups.set( - groupId, - TeamGroupModel.create({ - teamId: self.id, - groupId: groupId, - }) - ); - }), - - removeGroup: flow(function* load(groupId: string) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`); - self.groups.delete(groupId); - }), - })); - -type TeamType = typeof TeamModel.Type; -export interface Team extends TeamType {} - -export const TeamsStore = types - .model('TeamsStore', { - map: types.map(TeamModel), - search: types.optional(types.string, ''), - }) - .views(self => ({ - get filteredTeams(this: any) { - const teams = this.map.values(); - const regex = new RegExp(self.search, 'i'); - return teams.filter(team => { - return regex.test(team.name); - }); - }, - })) - .actions(self => ({ - loadTeams: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 }); - self.map.clear(); - - for (const team of rsp.teams) { - self.map.set(team.id.toString(), TeamModel.create(team)); - } - }), - - setSearchQuery(query: string) { - self.search = query; - }, - - loadById: flow(function* load(id: string) { - if (self.map.has(id)) { - return; - } - - const backendSrv = getEnv(self).backendSrv; - const team = yield backendSrv.get(`/api/teams/${id}`); - self.map.set(id, TeamModel.create(team)); - }), - })); diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index b101b31a2a2..0cdc07fd31a 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/alerting.ts b/public/app/types/alerting.ts new file mode 100644 index 00000000000..3a696d3a8d6 --- /dev/null +++ b/public/app/types/alerting.ts @@ -0,0 +1,35 @@ +export interface AlertRuleDTO { + id: number; + dashboardId: number; + dashboardUid: string; + dashboardSlug: string; + panelId: number; + name: string; + state: string; + newStateDate: string; + evalDate: string; + evalData?: object; + executionError: string; + url: string; +} + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + url: string; + info?: string; + executionError?: string; + evalData?: { noData: boolean }; +} + +export interface AlertRulesState { + items: AlertRule[]; + searchQuery: string; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..92bcdb32836 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,96 +1,30 @@ -// -// Location -// +import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; +import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; +import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; +import { NavModel, NavModelItem, NavIndex } from './navModel'; -export interface LocationUpdate { - path?: string; - query?: UrlQueryMap; - routeParams?: UrlQueryMap; -} - -export interface LocationState { - url: string; - path: string; - query: UrlQueryMap; - routeParams: UrlQueryMap; -} - -export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; -export type UrlQueryMap = { [s: string]: UrlQueryValue }; - -// -// Alerting -// - -export interface AlertRuleApi { - id: number; - dashboardId: number; - dashboardUid: string; - dashboardSlug: string; - panelId: number; - name: string; - state: string; - newStateDate: string; - evalDate: string; - evalData?: object; - executionError: string; - url: string; -} - -export interface AlertRule { - id: number; - dashboardId: number; - panelId: number; - name: string; - state: string; - stateText: string; - stateIcon: string; - stateClass: string; - stateAge: string; - url: string; - info?: string; - executionError?: string; - evalData?: { noData: boolean }; -} - -// -// NavModel -// - -export interface NavModelItem { - text: string; - url: string; - subTitle?: string; - icon?: string; - img?: string; - id: string; - active?: boolean; - hideFromTabs?: boolean; - divider?: boolean; - children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; - target?: string; - parentItem?: NavModelItem; -} - -export interface NavModel { - main: NavModelItem; - node: NavModelItem; -} - -export type NavIndex = { [s: string]: NavModelItem }; - -// -// Store -// - -export interface AlertRulesState { - items: AlertRule[]; - searchQuery: string; -} +export { + Team, + TeamsState, + TeamState, + TeamGroup, + TeamMember, + AlertRuleDTO, + AlertRule, + AlertRulesState, + LocationState, + LocationUpdate, + NavModel, + NavModelItem, + NavIndex, + UrlQueryMap, + UrlQueryValue, +}; export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; + team: TeamState; } diff --git a/public/app/types/location.ts b/public/app/types/location.ts new file mode 100644 index 00000000000..4a7f51523a7 --- /dev/null +++ b/public/app/types/location.ts @@ -0,0 +1,15 @@ +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} + +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts new file mode 100644 index 00000000000..aae4a030cb4 --- /dev/null +++ b/public/app/types/navModel.ts @@ -0,0 +1,22 @@ +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + breadcrumbs?: Array<{ title: string; url: string }>; + target?: string; + parentItem?: NavModelItem; +} + +export interface NavModel { + main: NavModelItem; + node: NavModelItem; +} + +export type NavIndex = { [s: string]: NavModelItem }; diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts new file mode 100644 index 00000000000..bcf752c86c0 --- /dev/null +++ b/public/app/types/teams.ts @@ -0,0 +1,32 @@ +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + +export interface TeamsState { + teams: Team[]; + searchQuery: string; +} + +export interface TeamState { + team: Team; + members: TeamMember[]; + groups: TeamGroup[]; + searchMemberQuery: string; +}