From 306c3e6c10fe755e58ddfe622749f4bd0f2c11bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 12:34:32 +0200 Subject: [PATCH 01/28] creating types, actions, reducer --- .../teams}/TeamGroupSync.tsx | 0 .../Teams => features/teams}/TeamList.tsx | 17 ++++++++- .../Teams => features/teams}/TeamMembers.tsx | 0 .../Teams => features/teams}/TeamPages.tsx | 0 .../Teams => features/teams}/TeamSettings.tsx | 0 public/app/features/teams/state/actions.ts | 28 +++++++++++++++ public/app/features/teams/state/reducers.ts | 14 ++++++++ public/app/features/teams/state/selectors.ts | 1 + public/app/routes/routes.ts | 4 +-- public/app/types/index.ts | 36 +++++++++++++++++++ 10 files changed, 97 insertions(+), 3 deletions(-) rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamList.tsx (89%) rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamPages.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamSettings.tsx (100%) create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.ts diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 100% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 89% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index d0feee75184..79d71c33596 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; @@ -6,6 +7,8 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { loadTeams } from './state/actions'; +import { getTeams } from './state/selectors'; interface Props { nav: typeof NavStore.Type; @@ -108,4 +111,16 @@ export class TeamList extends React.Component { } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + teams: getTeams(state), + }; +} + +function mapDispatchToProps() { + return { + loadTeams, + }; +} + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 100% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx similarity index 100% rename from public/app/containers/Teams/TeamPages.tsx rename to public/app/features/teams/TeamPages.tsx diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx similarity index 100% rename from public/app/containers/Teams/TeamSettings.tsx rename to public/app/features/teams/TeamSettings.tsx diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts new file mode 100644 index 00000000000..fafd2091217 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,28 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState, Team } from '../../../types'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export type Action = LoadTeamsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); + dispatch(teamsLoaded(teams)); + }; +} diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..a104ae2e21c --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,14 @@ +import { TeamsState } from '../../../types'; +import { Action } from './actions'; + +const initialState: TeamsState = { teams: [] }; + +export const teamsReducer = (state = initialState, action: Action): TeamsState => { + switch (action.type) { + } + return state; +}; + +export default { + teams: teamsReducer, +}; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..f1f66695e65 --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1 @@ +export const getTeams = state => state.teams; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index dfd215f7056..1fd1a474cd3 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..73cb05c26c1 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,9 @@ // Location // +import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; +import { types } from 'mobx-state-tree'; + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -53,6 +56,34 @@ export interface AlertRule { evalData?: { noData: boolean }; } +// +// Teams +// + +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; + search?: string; + members?: TeamMember[]; + groups?: TeamGroup[]; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + // // NavModel // @@ -89,8 +120,13 @@ export interface AlertRulesState { searchQuery: string; } +export interface TeamsState { + teams: Team[]; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; } From 167f0098193475a0ece4c280d26d1dfa4fee5d1a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:13:21 +0200 Subject: [PATCH 02/28] load teams and store in redux --- .../app/features/alerting/AlertRuleList.tsx | 4 +- public/app/features/teams/TeamList.test.tsx | 61 ++++++ public/app/features/teams/TeamList.tsx | 65 +++--- .../__snapshots__/TeamList.test.tsx.snap | 204 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 4 +- public/app/features/teams/state/reducers.ts | 4 +- public/app/stores/configureStore.ts | 2 + public/app/types/index.ts | 3 - 8 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 public/app/features/teams/TeamList.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4b48da47256..d30ba0ba802 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -115,7 +115,9 @@ export class AlertRuleList extends PureComponent {
    - {alertRules.map(rule => )} + {alertRules.map(rule => ( + {}} /> + ))}
diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..e7db3edfd3d --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamList, Props } from './TeamList'; +import { NavModel, Team } from '../../types'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + search: '', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamList; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render teams table', () => { + const { wrapper } = setup({ + teams: [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ], + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => {}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 79d71c33596..801b66d24fc 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,44 +1,38 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { NavModel, Team } from '../../types'; import { loadTeams } from './state/actions'; import { getTeams } from './state/selectors'; +import { getNavModel } from 'app/core/selectors/navModel'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - backendSrv: BackendSrv; +export interface Props { + navModel: NavModel; + teams: Team[]; + loadTeams: typeof loadTeams; + search: string; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - constructor(props) { - super(props); - - this.props.nav.load('cfg', 'teams'); +export class TeamList extends PureComponent { + componentDidMount() { this.fetchTeams(); } - fetchTeams() { - this.props.teams.loadTeams(); + async fetchTeams() { + await this.props.loadTeams(); } - deleteTeam(team: Team) { - this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); - } - - onSearchQueryChange = evt => { - this.props.teams.setSearchQuery(evt.target.value); + deleteTeam = (team: Team) => { + console.log('delete team', team); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + console.log('set search', event.target.value); + }; + + renderTeamMember(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -65,10 +59,11 @@ export class TeamList extends React.Component { } render() { - const { nav, teams } = this.props; + const { navModel, teams, search } = this.props; + return (
- +
@@ -77,7 +72,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -102,7 +97,7 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeamMember(team))}
@@ -113,14 +108,14 @@ export class TeamList extends React.Component { function mapStateToProps(state) { return { - teams: getTeams(state), + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + search: '', }; } -function mapDispatchToProps() { - return { - loadTeams, - }; -} +const mapDispatchToProps = { + loadTeams, +}; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap new file mode 100644 index 00000000000..c93dafde1c6 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + +
+ + Name + + Email + + Members + +
+
+
+
+`; + +exports[`Render should render teams table 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test + + + + test@test.com + + + + 1 + + + +
+
+
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index fafd2091217..35853bd73e8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -22,7 +22,7 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); - dispatch(teamsLoaded(teams)); + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a104ae2e21c..968c69d862c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,10 +1,12 @@ import { TeamsState } from '../../../types'; -import { Action } from './actions'; +import { Action, ActionTypes } from './actions'; const initialState: TeamsState = { teams: [] }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { + case ActionTypes.LoadTeams: + return { teams: action.payload }; } return state; }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 232f2e30cb8..a79c59a5fc1 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -3,10 +3,12 @@ import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; +import teamsReducers from 'app/features/teams/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, + ...teamsReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 73cb05c26c1..beb3253787a 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,9 +2,6 @@ // Location // -import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; -import { types } from 'mobx-state-tree'; - export interface LocationUpdate { path?: string; query?: UrlQueryMap; From 7e340b7aa5c1016c934a68d33597672e38e36584 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:32:51 +0200 Subject: [PATCH 03/28] delete team --- public/app/features/teams/TeamList.test.tsx | 38 +++++++++++++-------- public/app/features/teams/TeamList.tsx | 6 ++-- public/app/features/teams/state/actions.ts | 10 ++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index e7db3edfd3d..7e4caff80ae 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TeamList, Props } from './TeamList'; +import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; const setup = (propOverrides?: object) => { @@ -8,6 +8,7 @@ const setup = (propOverrides?: object) => { navModel: {} as NavModel, teams: [] as Team[], loadTeams: jest.fn(), + deleteTeam: jest.fn(), search: '', }; @@ -22,6 +23,17 @@ const setup = (propOverrides?: object) => { }; }; +const mockTeam: Team = { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], +}; + describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -30,18 +42,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ], + teams: [mockTeam], }); expect(wrapper).toMatchSnapshot(); @@ -58,4 +59,13 @@ describe('Life cycle', () => { }); }); -describe('Functions', () => {}); +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(mockTeam); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 801b66d24fc..df8776920d7 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams } from './state/actions'; +import { loadTeams, deleteTeam } from './state/actions'; import { getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -12,6 +12,7 @@ export interface Props { navModel: NavModel; teams: Team[]; loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; search: string; } @@ -25,7 +26,7 @@ export class TeamList extends PureComponent { } deleteTeam = (team: Team) => { - console.log('delete team', team); + this.props.deleteTeam(team.id); }; onSearchQueryChange = event => { @@ -116,6 +117,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadTeams, + deleteTeam, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35853bd73e8..6afed1828c1 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -26,3 +26,13 @@ export function loadTeams(): ThunkResult { dispatch(teamsLoaded(response.teams)); }; } + +export function deleteTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} From f68ac2021873bcc827de00423a5af90daab34faa Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 16:49:36 +0200 Subject: [PATCH 04/28] set search query action and tests --- public/app/features/teams/TeamList.test.tsx | 30 +-- public/app/features/teams/TeamList.tsx | 20 +- .../app/features/teams/__mocks__/teamMocks.ts | 32 +++ .../__snapshots__/TeamList.test.tsx.snap | 204 +++++++++++++++++- public/app/features/teams/state/actions.ts | 13 +- .../app/features/teams/state/reducers.test.ts | 41 ++++ public/app/features/teams/state/reducers.ts | 7 +- .../features/teams/state/selectors.test.ts | 25 +++ public/app/features/teams/state/selectors.ts | 10 +- public/app/types/index.ts | 1 + 10 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 7e4caff80ae..6c12f1357e5 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; +import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { @@ -9,7 +10,8 @@ const setup = (propOverrides?: object) => { teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), - search: '', + setSearchQuery: jest.fn(), + searchQuery: '', }; Object.assign(props, propOverrides); @@ -23,17 +25,6 @@ const setup = (propOverrides?: object) => { }; }; -const mockTeam: Team = { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], -}; - describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -42,7 +33,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [mockTeam], + teams: getMultipleMockTeams(5), }); expect(wrapper).toMatchSnapshot(); @@ -63,9 +54,20 @@ describe('Functions', () => { describe('Delete team', () => { it('should call delete team', () => { const { instance } = setup(); - instance.deleteTeam(mockTeam); + instance.deleteTeam(getMockTeam()); expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); }); }); + + describe('on search query change', () => { + it('should call setSearchQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'test' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test'); + }); + }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index df8776920d7..a95f0f17a27 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,8 +4,8 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams, deleteTeam } from './state/actions'; -import { getTeams } from './state/selectors'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; export interface Props { @@ -13,7 +13,8 @@ export interface Props { teams: Team[]; loadTeams: typeof loadTeams; deleteTeam: typeof deleteTeam; - search: string; + setSearchQuery: typeof setSearchQuery; + searchQuery: string; } export class TeamList extends PureComponent { @@ -30,10 +31,10 @@ export class TeamList extends PureComponent { }; onSearchQueryChange = event => { - console.log('set search', event.target.value); + this.props.setSearchQuery(event.target.value); }; - renderTeamMember(team: Team) { + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -60,7 +61,7 @@ export class TeamList extends PureComponent { } render() { - const { navModel, teams, search } = this.props; + const { navModel, teams, searchQuery } = this.props; return (
@@ -73,7 +74,7 @@ export class TeamList extends PureComponent { type="text" className="gf-form-input" placeholder="Search teams" - value={search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -98,7 +99,7 @@ export class TeamList extends PureComponent { - {teams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
@@ -111,13 +112,14 @@ function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'teams'), teams: getTeams(state.teams), - search: '', + searchQuery: getSearchQuery(state.teams), }; } const mapDispatchToProps = { loadTeams, deleteTeam, + setSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..34405d2ce91 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,32 @@ +import { Team } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + let teams: Team[] = []; + for (let i = 1; i <= numberOfTeams; i++) { + teams.push({ + id: i, + name: `test-${i}`, + avatarUrl: 'some/url/', + email: `test-${i}@test.com`, + memberCount: i, + search: '', + members: [], + groups: [], + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index c93dafde1c6..6ea189f5dbd 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -167,7 +167,7 @@ exports[`Render should render teams table 1`] = ` - test + test-1 - test@test.com + test-1@test.com + + + + + + + + + test-2 + + + + + test-2@test.com + + + + + 2 + + + + + + + + + + + + + + + test-3 + + + + + test-3@test.com + + + + + 3 + + + + + + + + + + + + + + + test-4 + + + + + test-4@test.com + + + + + 4 + + + + + + + + + + + + + + + test-5 + + + + + test-5@test.com + + + + + 5 + + + + + +
diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 6afed1828c1..5914a932ad0 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -4,6 +4,7 @@ import { StoreState, Team } from '../../../types'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + SetSearchQuery = 'SET_SEARCH_QUERY', } export interface LoadTeamsAction { @@ -11,7 +12,12 @@ export interface LoadTeamsAction { payload: Team[]; } -export type Action = LoadTeamsAction; +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export type Action = LoadTeamsAction | SetSearchQueryAction; type ThunkResult = ThunkAction; @@ -20,6 +26,11 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ payload: teams, }); +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + export function loadTeams(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts new file mode 100644 index 00000000000..e115d311e37 --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,41 @@ +import { Action, ActionTypes } from './actions'; +import { initialState, teamsReducer } from './reducers'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.searchQuery).toEqual('test'); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 968c69d862c..673fd240668 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,12 +1,15 @@ import { TeamsState } from '../../../types'; import { Action, ActionTypes } from './actions'; -const initialState: TeamsState = { teams: [] }; +export const initialState: TeamsState = { teams: [], searchQuery: '' }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: - return { teams: action.payload }; + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; } return state; }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts new file mode 100644 index 00000000000..66fd07444ce --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,25 @@ +import { getTeams } from './selectors'; +import { getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState } from '../../../types'; + +describe('Team selectors', () => { + describe('Get teams', () => { + const mockTeams = getMultipleMockTeams(5); + + it('should return teams if no search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '' }; + + const teams = getTeams(mockState); + + expect(teams).toEqual(mockTeams); + }); + + it('Should filter teams if search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' }; + + const teams = getTeams(mockState); + + expect(teams.length).toEqual(1); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index f1f66695e65..632bb2cd02a 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1 +1,9 @@ -export const getTeams = state => state.teams; +export const getSearchQuery = state => state.searchQuery; + +export const getTeams = state => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index beb3253787a..b867a8f6989 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -119,6 +119,7 @@ export interface AlertRulesState { export interface TeamsState { teams: Team[]; + searchQuery: string; } export interface StoreState { From 05bfc3651626b82845626820e23c73e4fd57b2ce Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH 05/28] Teampages page --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 14 ++- public/app/core/reducers/navModel.ts | 18 ++- public/app/core/selectors/location.ts | 3 + public/app/core/selectors/navModel.ts | 2 +- public/app/features/teams/TeamGroupSync.tsx | 12 +- public/app/features/teams/TeamMembers.tsx | 27 ++--- public/app/features/teams/TeamPages.test.tsx | 63 +++++++++++ public/app/features/teams/TeamPages.tsx | 107 +++++++++++------- public/app/features/teams/TeamSettings.tsx | 10 +- .../features/teams/__mocks__/navModelMock.ts | 59 ++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 87 ++++++++++++++ public/app/features/teams/state/actions.ts | 57 +++++++++- .../app/features/teams/state/reducers.test.ts | 6 +- public/app/features/teams/state/reducers.ts | 17 ++- public/app/features/teams/state/selectors.ts | 2 + public/app/types/index.ts | 7 +- 17 files changed, 410 insertions(+), 84 deletions(-) create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..b4b9b21126e 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { updateLocation } from './location'; +import { updateNavIndex } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..96465c6ef60 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,3 +1,9 @@ +import { NavModelItem } from '../../types'; + +export enum ActionTypes { + UpdateNavIndex = 'UPDATE_NAV_INDEX', +} + export type Action = UpdateNavIndexAction; // this action is not used yet @@ -5,9 +11,11 @@ export type Action = UpdateNavIndexAction; // like datasource edit, teams edit page export interface UpdateNavIndexAction { - type: 'UPDATE_NAV_INDEX'; + type: ActionTypes.UpdateNavIndex; + payload: NavModelItem; } -export const updateNavIndex = (): UpdateNavIndexAction => ({ - type: 'UPDATE_NAV_INDEX', +export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({ + type: ActionTypes.UpdateNavIndex, + payload: item, }); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 26acdb39a3d..ac0e51854e7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ -import { Action } from 'app/core/actions/navModel'; -import { NavModelItem, NavIndex } from 'app/types'; +import { Action, ActionTypes } from 'app/core/actions/navModel'; +import { NavIndex, NavModelItem } from 'app/types'; import config from 'app/core/config'; export function buildInitialState(): NavIndex { @@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem? export const initialState: NavIndex = buildInitialState(); export const navIndexReducer = (state = initialState, action: Action): NavIndex => { + switch (action.type) { + case ActionTypes.UpdateNavIndex: + const newPages = {}; + const payload = action.payload; + + for (const node of payload.children) { + newPages[node.id] = { + ...node, + parentItem: payload, + }; + } + + return { ...state, ...newPages }; + } return state; }; diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts new file mode 100644 index 00000000000..adc31f47e89 --- /dev/null +++ b/public/app/core/selectors/location.ts @@ -0,0 +1,3 @@ +export const getRouteParamsId = state => state.routeParams.id; + +export const getRouteParamsPage = state => state.routeParams.page; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index a7e1c3330bd..8b3a3edd84e 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,7 +1,7 @@ import { NavModel, NavModelItem, NavIndex } from 'app/types'; function getNotFoundModel(): NavModel { - var node: NavModelItem = { + const node: NavModelItem = { id: 'not-found', text: 'Page not found', icon: 'fa fa-fw fa-warning', diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..6562820d717 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Team, TeamGroup } from '../../types'; interface Props { team: Team; @@ -16,7 +15,6 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -@observer export class TeamGroupSync extends React.Component { constructor(props) { super(props); @@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component { } componentDidMount() { - this.props.team.loadGroups(); + // this.props.team.loadGroups(); } renderGroup(group: TeamGroup) { @@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component { }; onAddGroup = () => { - this.props.team.addGroup(this.state.newGroupId); + // this.props.team.addGroup(this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.team.removeGroup(group.groupId); + // this.props.team.removeGroup(group.groupId); }; isNewGroupValid() { @@ -63,7 +61,7 @@ export class TeamGroupSync extends React.Component { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.team.groups; return (
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index b06a547063a..32eb0d09b63 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { Team, TeamMember } from '../../types'; interface Props { team: Team; @@ -15,27 +14,26 @@ interface State { newTeamMember?: User; } -@observer -export class TeamMembers extends React.Component { +export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; } componentDidMount() { - this.props.team.loadMembers(); + // this.props.team.loadMembers(); } onSearchQueryChange = evt => { - this.props.team.setSearchQuery(evt.target.value); + // this.props.team.setSearchQuery(evt.target.value); }; removeMember(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } removeMemberConfirmed(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } renderMember(member: TeamMember) { @@ -62,16 +60,15 @@ export class TeamMembers extends React.Component { }; onAddUserToTeam = async () => { - await this.props.team.addMember(this.state.newTeamMember.id); - await this.props.team.loadMembers(); - this.setState({ newTeamMember: null }); + // await this.props.team.addMember(this.state.newTeamMember.id); + // await this.props.team.loadMembers(); + // this.setState({ newTeamMember: null }); }; render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.filteredMembers; - const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); const { team } = this.props; + const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return (
@@ -124,7 +121,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {team.members && team.members.map(member => this.renderMember(member))}
diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx new file mode 100644 index 00000000000..65084d0dc47 --- /dev/null +++ b/public/app/features/teams/TeamPages.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamPages, Props } from './TeamPages'; +import { NavModel, Team } from '../../types'; +import { getMockTeam } from './__mocks__/teamMocks'; + +jest.mock('app/core/config', () => ({ + buildInfo: { isEnterprise: true }, +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teamId: 1, + loadTeam: jest.fn(), + pageName: 'members', + team: {} as Team, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance(); + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render member page if team not empty', () => { + const { wrapper } = setup({ + team: getMockTeam(), + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render settings page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render group sync page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'groupsync', + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2abc9c51535..606e254e7ec 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,77 +1,106 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import _ from 'lodash'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import config from 'app/core/config'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { ViewStore } from 'app/stores/ViewStore/ViewStore'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; +import { NavModel, Team } from '../../types'; +import { loadTeam } from './state/actions'; +import { getTeam } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - view: typeof ViewStore.Type; +export interface Props { + team: Team; + loadTeam: typeof loadTeam; + teamId: number; + pageName: string; + navModel: NavModel; } -@inject('nav', 'teams', 'view') -@observer -export class TeamPages extends React.Component { +interface State { isSyncEnabled: boolean; - currentPage: string; +} +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { constructor(props) { super(props); - this.isSyncEnabled = config.buildInfo.isEnterprise; - this.currentPage = this.getCurrentPage(); + this.state = { + isSyncEnabled: config.buildInfo.isEnterprise, + }; + } + componentDidMount() { this.loadTeam(); } async loadTeam() { - const { teams, nav, view } = this.props; + const { loadTeam, teamId } = this.props; - await teams.loadById(view.routeParams.get('id')); - - nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); - } - - getCurrentTeam(): Team { - const { teams, view } = this.props; - return teams.map.get(view.routeParams.get('id')); + await loadTeam(teamId); } getCurrentPage() { const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.view.routeParams.get('page'); + const currentPage = this.props.pageName; return _.includes(pages, currentPage) ? currentPage : pages[0]; } - render() { - const { nav } = this.props; - const currentTeam = this.getCurrentTeam(); + renderPage() { + const { team } = this.props; + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); - if (!nav.main) { - return null; + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; } + return null; + } + + render() { + const { team, navModel } = this.props; + return (
- - {currentTeam && ( -
- {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
- )} + + {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
}
); } } -export default hot(module)(TeamPages); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + const pageName = getRouteParamsPage(state.location) || 'members'; + + return { + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + teamId: teamId, + pageName: pageName, + team: getTeam(state.team), + }; +} + +const mapDispatchToProps = { + loadTeam, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages)); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 0de60a0b16c..6e3c90d93f9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,30 +1,28 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team } from 'app/stores/TeamsStore/TeamsStore'; import { Label } from 'app/core/components/Forms/Forms'; +import { Team } from '../../types'; interface Props { team: Team; } -@observer export class TeamSettings extends React.Component { constructor(props) { super(props); } onChangeName = evt => { - this.props.team.setName(evt.target.value); + // this.props.team.setName(evt.target.value); }; onChangeEmail = evt => { - this.props.team.setEmail(evt.target.value); + // this.props.team.setEmail(evt.target.value); }; onUpdate = evt => { evt.preventDefault(); - this.props.team.update(); + // this.props.team.update(); }; render() { diff --git a/public/app/features/teams/__mocks__/navModelMock.ts b/public/app/features/teams/__mocks__/navModelMock.ts new file mode 100644 index 00000000000..7aa8515ee13 --- /dev/null +++ b/public/app/features/teams/__mocks__/navModelMock.ts @@ -0,0 +1,59 @@ +export const getMockNavModel = (pageName: string) => { + return { + node: { + active: false, + icon: 'gicon gicon-team', + id: `team-${pageName}-2`, + text: `${pageName}`, + url: 'org/teams/edit/2/members', + parentItem: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }, + main: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: true, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap new file mode 100644 index 00000000000..3c19d726e41 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+`; + +exports[`Render should render group sync page 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render member page if team not empty 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render settings page 1`] = ` +
+ +
+ +
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5914a932ad0..35d07157dec 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,9 +1,12 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team } from '../../../types'; +import { updateNavIndex } from '../../../core/actions'; +import { UpdateNavIndexAction } from '../../../core/actions/navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', } @@ -12,20 +15,30 @@ export interface LoadTeamsAction { payload: Team[]; } +export interface LoadTeamAction { + type: ActionTypes.LoadTeam; + payload: Team; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction; +export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ type: ActionTypes.LoadTeams, payload: teams, }); +const teamLoaded = (team: Team): LoadTeamAction => ({ + type: ActionTypes.LoadTeam, + payload: team, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -38,6 +51,44 @@ export function loadTeams(): ThunkResult { }; } +function buildNavModel(team: Team): NavModelItem { + return { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; +} + +export function loadTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index e115d311e37..0ab64a78e41 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { initialState, teamsReducer } from './reducers'; +import { initialTeamsState, teamsReducer } from './reducers'; describe('teams reducer', () => { it('should set teams', () => { @@ -21,7 +21,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.teams).toEqual(payload); }); @@ -34,7 +34,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.searchQuery).toEqual('test'); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 673fd240668..56a2f83cd8d 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,9 +1,10 @@ -import { TeamsState } from '../../../types'; +import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; -export const initialState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; -export const teamsReducer = (state = initialState, action: Action): TeamsState => { +export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: return { ...state, teams: action.payload }; @@ -14,6 +15,16 @@ export const teamsReducer = (state = initialState, action: Action): TeamsState = return state; }; +export const teamReducer = (state = initialTeamState, action: Action): TeamState => { + switch (action.type) { + case ActionTypes.LoadTeam: + return { ...state, team: action.payload }; + } + + return state; +}; + export default { teams: teamsReducer, + team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 632bb2cd02a..40940cbae52 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,7 @@ export const getSearchQuery = state => state.searchQuery; +export const getTeam = state => state.team; + export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b867a8f6989..8b0010b6561 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -96,7 +96,7 @@ export interface NavModelItem { hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; + breadcrumbs?: { title: string; url: string }[]; target?: string; parentItem?: NavModelItem; } @@ -122,6 +122,11 @@ export interface TeamsState { searchQuery: string; } +export interface TeamState { + team: Team; + searchQuery: string; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; From 59b3bfd34293e46ef19c091e45f6be7d16fd309b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 18:01:59 +0200 Subject: [PATCH 06/28] team members, bug in fetching team --- public/app/features/teams/TeamMembers.tsx | 68 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 10 +++ public/app/features/teams/state/actions.ts | 67 +++++++++++++++++- .../app/features/teams/state/reducers.test.ts | 37 ++++++---- public/app/features/teams/state/reducers.ts | 8 ++- public/app/features/teams/state/selectors.ts | 8 ++- public/app/types/index.ts | 3 +- 8 files changed, 161 insertions(+), 44 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 32eb0d09b63..115fb40e184 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,12 +1,21 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { Team, TeamMember } from '../../types'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeam } from './state/selectors'; +import { getRouteParamsId } from '../../core/selectors/location'; interface Props { team: Team; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -21,20 +30,29 @@ export class TeamMembers extends PureComponent { } componentDidMount() { - // this.props.team.loadMembers(); + this.props.loadTeamMembers(); } - onSearchQueryChange = evt => { - // this.props.team.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + this.props.setSearchMemberQuery(event.target.value); }; removeMember(member: TeamMember) { - // this.props.team.removeMember(member); + this.props.removeTeamMember(member.userId); } - removeMemberConfirmed(member: TeamMember) { - // this.props.team.removeMember(member); - } + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + this.props.addTeamMember(this.state.newTeamMember.id); + this.setState({ newTeamMember: null }); + }; renderMember(member: TeamMember) { return ( @@ -51,23 +69,9 @@ export class TeamMembers extends PureComponent { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onUserSelected = (user: User) => { - this.setState({ newTeamMember: user }); - }; - - onAddUserToTeam = async () => { - // await this.props.team.addMember(this.state.newTeamMember.id); - // await this.props.team.loadMembers(); - // this.setState({ newTeamMember: null }); - }; - render() { const { newTeamMember, isAdding } = this.state; - const { team } = this.props; + const { team, searchMemberQuery } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -79,7 +83,7 @@ export class TeamMembers extends PureComponent { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -129,4 +133,20 @@ export class TeamMembers extends PureComponent { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 606e254e7ec..4395c0bfbef 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -63,7 +63,7 @@ export class TeamPages extends PureComponent { switch (currentPage) { case PageTypes.Members: - return ; + return ; case PageTypes.Settings: return ; @@ -95,7 +95,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), teamId: teamId, pageName: pageName, - team: getTeam(state.team), + team: getTeam(state.team, teamId), }; } diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 34405d2ce91..21c0cf012f0 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -30,3 +30,13 @@ export const getMockTeam = (): Team => { groups: [], }; }; + +export const getMockTeamMember = () => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35d07157dec..e407737bb20 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,6 +1,6 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; @@ -8,6 +8,8 @@ export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', } export interface LoadTeamsAction { @@ -20,12 +22,27 @@ export interface LoadTeamAction { payload: Team; } +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction; type ThunkResult = ThunkAction; @@ -39,6 +56,16 @@ const teamLoaded = (team: Team): LoadTeamAction => ({ payload: team, }); +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -89,6 +116,42 @@ export function loadTeam(id: number): ThunkResult { }; } +export function loadTeamMembers(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/members`) + .then(response => { + dispatch(teamMembersLoaded(response)); + }); + }; +} + +export function addTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/members`, { userId: id }) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + +export function removeTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/members/${id}`) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 0ab64a78e41..492ec71ba4b 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,20 +1,10 @@ import { Action, ActionTypes } from './actions'; -import { initialTeamsState, teamsReducer } from './reducers'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; describe('teams reducer', () => { it('should set teams', () => { - const payload = [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ]; + const payload = [getMockTeam()]; const action: Action = { type: ActionTypes.LoadTeams, @@ -39,3 +29,24 @@ describe('teams reducer', () => { expect(result.searchQuery).toEqual('test'); }); }); + +describe('team reducer', () => { + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + const mockTeam = getMockTeam(); + const state = { + ...initialTeamState, + team: mockTeam, + }; + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(state, action); + const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + + expect(result).toEqual(expectedState); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 56a2f83cd8d..e30fddb22a5 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -19,6 +19,12 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState switch (action.type) { case ActionTypes.LoadTeam: return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, team: { ...state.team, members: action.payload } }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 40940cbae52..d6142adf157 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,6 +1,12 @@ export const getSearchQuery = state => state.searchQuery; +export const getSearchMemberQuery = state => state.searchMemberQuery; -export const getTeam = state => state.team; +export const getTeam = (state, currentTeamId) => { + if (state.team.id === currentTeamId) { + console.log('yes'); + return state.team; + } +}; export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8b0010b6561..27ae3dbe19b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -124,7 +124,7 @@ export interface TeamsState { export interface TeamState { team: Team; - searchQuery: string; + searchMemberQuery: string; } export interface StoreState { @@ -132,4 +132,5 @@ export interface StoreState { location: LocationState; alertRules: AlertRulesState; teams: TeamsState; + team: TeamState; } From d494ebc7309b363a8d131b1d719131864196e04c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:19:27 +0200 Subject: [PATCH 07/28] flattened team state, tests for TeamMembers --- .../app/features/teams/TeamMembers.test.tsx | 79 +++++ public/app/features/teams/TeamMembers.tsx | 24 +- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 26 +- .../__snapshots__/TeamMembers.test.tsx.snap | 317 ++++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 21 +- public/app/features/teams/state/actions.ts | 1 + .../app/features/teams/state/reducers.test.ts | 36 +- public/app/features/teams/state/reducers.ts | 11 +- .../features/teams/state/selectors.test.ts | 22 +- public/app/features/teams/state/selectors.ts | 11 +- public/app/types/index.ts | 5 +- 12 files changed, 493 insertions(+), 64 deletions(-) create mode 100644 public/app/features/teams/TeamMembers.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx new file mode 100644 index 00000000000..cae37e184fb --- /dev/null +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamMembers, Props } from './TeamMembers'; +import { TeamMember } from '../../types'; +import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + members: [] as TeamMember[], + searchMemberQuery: '', + setSearchMemberQuery: jest.fn(), + loadTeamMembers: jest.fn(), + addTeamMember: jest.fn(), + removeTeamMember: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamMembers; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render team members', () => { + const { wrapper } = setup({ + members: getMockTeamMembers(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + describe('on search member query change', () => { + it('it should call setSearchMemberQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'member' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member'); + }); + }); + + describe('on remove member', () => { + const { instance } = setup(); + const mockTeamMember = getMockTeamMember(); + + instance.onRemoveMember(mockTeamMember); + + expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1); + }); + + describe('on add user to team', () => { + const { wrapper, instance } = setup(); + + wrapper.state().newTeamMember = { + id: 1, + label: '', + avatarUrl: '', + login: '', + }; + + instance.onAddUserToTeam(); + + expect(instance.props.addTeamMember).toHaveBeenCalledWith(1); + }); +}); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 115fb40e184..5ad688aabf8 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,16 +1,14 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import { hot } from 'react-hot-loader'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; -import { Team, TeamMember } from '../../types'; +import { TeamMember } from '../../types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; -import { getSearchMemberQuery, getTeam } from './state/selectors'; -import { getRouteParamsId } from '../../core/selectors/location'; +import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + members: TeamMember[]; searchMemberQuery: string; loadTeamMembers: typeof loadTeamMembers; addTeamMember: typeof addTeamMember; @@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent { this.props.setSearchMemberQuery(event.target.value); }; - removeMember(member: TeamMember) { + onRemoveMember(member: TeamMember) { this.props.removeTeamMember(member.userId); } @@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - this.removeMember(member)} /> + this.onRemoveMember(member)} /> ); @@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent { render() { const { newTeamMember, isAdding } = this.state; - const { team, searchMemberQuery } = this.props; + const { searchMemberQuery, members } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent { - {team.members && team.members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member))}
@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent { } function mapStateToProps(state) { - const teamId = getRouteParamsId(state.location); - return { - team: getTeam(state.team, teamId), + members: getTeamMembers(state.team), searchMemberQuery: getSearchMemberQuery(state.team), }; } @@ -149,4 +145,4 @@ const mapDispatchToProps = { setSearchMemberQuery, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 4395c0bfbef..2528c3c87b8 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -41,10 +41,10 @@ export class TeamPages extends PureComponent { } componentDidMount() { - this.loadTeam(); + this.fetchTeam(); } - async loadTeam() { + async fetchTeam() { const { loadTeam, teamId } = this.props; await loadTeam(teamId); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 21c0cf012f0..7050997c387 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team } from '../../../types'; +import { Team, TeamMember } from '../../../types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { let teams: Team[] = []; @@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { avatarUrl: 'some/url/', email: `test-${i}@test.com`, memberCount: i, - search: '', - members: [], - groups: [], }); } @@ -25,13 +22,26 @@ export const getMockTeam = (): Team => { avatarUrl: 'some/url/', email: 'test@test.com', memberCount: 1, - search: '', - members: [], - groups: [], }; }; -export const getMockTeamMember = () => { +export const getMockTeamMembers = (amount: number): TeamMember[] => { + let teamMembers: TeamMember[] = []; + + for (let i = 1; i <= amount; i++) { + teamMembers.push({ + userId: i, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: `testUser-${i}`, + }); + } + + return teamMembers; +}; + +export const getMockTeamMember = (): TeamMember => { return { userId: 1, teamId: 1, diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap new file mode 100644 index 00000000000..2a42897e2b9 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + +
+ + Name + + Email + +
+
+
+`; + +exports[`Render should render team members 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + +
+ + + testUser-1 + + test@test.com + + +
+ + + testUser-2 + + test@test.com + + +
+ + + testUser-3 + + test@test.com + + +
+ + + testUser-4 + + test@test.com + + +
+ + + testUser-5 + + test@test.com + + +
+
+
+`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 3c19d726e41..563d3d3bb99 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> @@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
- +
`; @@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e407737bb20..4786edf60a8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { + console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 492ec71ba4b..7f7a33d60ac 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -31,22 +31,42 @@ describe('teams reducer', () => { }); describe('team reducer', () => { + it('should set team', () => { + const payload = getMockTeam(); + + const action: Action = { + type: ActionTypes.LoadTeam, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.team).toEqual(payload); + }); + it('should set team members', () => { const mockTeamMember = getMockTeamMember(); - const mockTeam = getMockTeam(); - const state = { - ...initialTeamState, - team: mockTeam, - }; const action: Action = { type: ActionTypes.LoadTeamMembers, payload: [mockTeamMember], }; - const result = teamReducer(state, action); - const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + const result = teamReducer(initialTeamState, action); - expect(result).toEqual(expectedState); + expect(result.members).toEqual([mockTeamMember]); + }); + + it('should set member search query', () => { + const payload = 'member'; + + const action: Action = { + type: ActionTypes.SetSearchMemberQuery, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.searchMemberQuery).toEqual('member'); }); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index e30fddb22a5..f02ade60923 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,8 +1,13 @@ -import { Team, TeamsState, TeamState } from '../../../types'; +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; +export const initialTeamState: TeamState = { + team: {} as Team, + members: [] as TeamMember[], + groups: [] as TeamGroup[], + searchMemberQuery: '', +}; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState return { ...state, team: action.payload }; case ActionTypes.LoadTeamMembers: - return { ...state, team: { ...state.team, members: action.payload } }; + return { ...state, members: action.payload }; case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 66fd07444ce..e1b11cf288b 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,8 +1,8 @@ -import { getTeams } from './selectors'; -import { getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState } from '../../../types'; +import { getTeam, getTeams } from './selectors'; +import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState, TeamState } from '../../../types'; -describe('Team selectors', () => { +describe('Teams selectors', () => { describe('Get teams', () => { const mockTeams = getMultipleMockTeams(5); @@ -23,3 +23,17 @@ describe('Team selectors', () => { }); }); }); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index d6142adf157..5e22f96eaf7 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; export const getTeam = (state, currentTeamId) => { - if (state.team.id === currentTeamId) { - console.log('yes'); + if (state.team.id === parseInt(currentTeamId)) { return state.team; } }; @@ -15,3 +14,11 @@ export const getTeams = state => { return regex.test(team.name); }); }; + +export const getTeamMembers = state => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 27ae3dbe19b..35cd9a41f4e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -63,9 +63,6 @@ export interface Team { avatarUrl: string; email: string; memberCount: number; - search?: string; - members?: TeamMember[]; - groups?: TeamGroup[]; } export interface TeamMember { @@ -124,6 +121,8 @@ export interface TeamsState { export interface TeamState { team: Team; + members: TeamMember[]; + groups: TeamGroup[]; searchMemberQuery: string; } From 841bd5817de3102c7310625aff94ed6d58d03bb2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:27:33 +0200 Subject: [PATCH 08/28] test for team member selector --- .../features/teams/state/selectors.test.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index e1b11cf288b..5f338069bbb 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,6 +1,6 @@ -import { getTeam, getTeams } from './selectors'; -import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState, TeamState } from '../../../types'; +import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; describe('Teams selectors', () => { describe('Get teams', () => { @@ -29,11 +29,28 @@ describe('Team selectors', () => { const mockTeam = getMockTeam(); it('should return team if matching with location team', () => { - const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; const team = getTeam(mockState, '1'); expect(team).toEqual(mockTeam); }); }); + + describe('Get members', () => { + const mockTeamMembers = getMockTeamMembers(5); + + it('should return team members', () => { + const mockState: TeamState = { + team: {} as Team, + searchMemberQuery: '', + members: mockTeamMembers, + groups: [] as TeamGroup[], + }; + + const members = getTeamMembers(mockState); + + expect(members).toEqual(mockTeamMembers); + }); + }); }); From 59b5b146daaa7655a0a77594206a6c637db7041a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 14:12:38 +0200 Subject: [PATCH 09/28] wip: began folder to redux migration --- .../ManageDashboards/FolderSettings.tsx | 160 ---------------- .../FolderSettingsPage.test.tsx} | 0 .../manage-dashboards/FolderSettingsPage.tsx | 180 ++++++++++++++++++ .../manage-dashboards/state/actions.ts | 29 +++ .../manage-dashboards/state/reducers.ts | 0 public/app/routes/routes.ts | 4 +- public/app/types/dashboard.ts | 7 + public/app/types/index.ts | 4 + 8 files changed, 222 insertions(+), 162 deletions(-) delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx rename public/app/{containers/ManageDashboards/FolderSettings.test.tsx => features/manage-dashboards/FolderSettingsPage.test.tsx} (100%) create mode 100644 public/app/features/manage-dashboards/FolderSettingsPage.tsx create mode 100644 public/app/features/manage-dashboards/state/actions.ts create mode 100644 public/app/features/manage-dashboards/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

Loading

; - } - - return ( -
- -
-

Folder Settings

- -
-
-
- - -
-
- - -
- -
-
-
- ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx similarity index 100% rename from public/app/containers/ManageDashboards/FolderSettings.test.tsx rename to public/app/features/manage-dashboards/FolderSettingsPage.test.tsx diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx new file mode 100644 index 00000000000..4ed6743a8dc --- /dev/null +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -0,0 +1,180 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState } from 'app/types'; +import { getFolderByUid } from './state/actions'; + +export interface Props { + navModel: NavModel; + folderUid: string; + getFolderByUid: typeof getFolderByUid; +} + +export class FolderSettingsPage extends PureComponent { + // formSnapshot: any; + // + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + // + // loadStore() { + // const { nav, folder, view } = this.props; + // + // return folder.load(view.routeParams.get('uid') as string).then(res => { + // this.formSnapshot = getSnapshot(folder); + // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); + // + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // } + + // onTitleChange(evt) { + // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); + // } + // + // getFormSnapshot() { + // if (!this.formSnapshot) { + // this.formSnapshot = getSnapshot(this.props.folder); + // } + // + // return this.formSnapshot; + // } + // + // save(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { nav, folder, view } = this.props; + // + // folder + // .saveFolder({ overwrite: false }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }) + // .catch(this.handleSaveFolderError.bind(this)); + // } + // + // delete(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { folder, view } = this.props; + // const title = folder.folder.title; + // + // appEvents.emit('confirm-modal', { + // title: 'Delete', + // text: `Do you want to delete this folder and all its dashboards?`, + // icon: 'fa-trash', + // yesText: 'Delete', + // onConfirm: () => { + // return folder.deleteFolder().then(() => { + // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); + // view.updatePathAndQuery('dashboards', '', ''); + // }); + // }, + // }); + // } + // + // handleSaveFolderError(err) { + // if (err.data && err.data.status === 'version-mismatch') { + // err.isHandled = true; + // + // const { nav, folder, view } = this.props; + // + // appEvents.emit('confirm-modal', { + // title: 'Conflict', + // text: 'Someone else has updated this folder.', + // text2: 'Would you still like to save this folder?', + // yesText: 'Save & Overwrite', + // icon: 'fa-warning', + // onConfirm: () => { + // folder + // .saveFolder({ overwrite: true }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // }, + // }); + // } + // } + + render() { + const { navModel } = this.props; + + // if (!folder.folder || !nav.main) { + // return

Loading

; + // } + + return ( +
+ +
+

Folder Settings

+
+
+ ); + } + + // asd() { + //
+ //
+ //
+ // + // + //
+ //
+ // + // + //
+ // + //
+ // + // } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + folderUid: uid, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts new file mode 100644 index 00000000000..ab5e1212d5f --- /dev/null +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -0,0 +1,29 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO } from 'app/types'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export type Action = LoadFolderAction; + +type ThunkResult = ThunkAction; + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a0b070cbcb4..4c50bd65de9 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,7 +4,7 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; import TeamPages from 'app/containers/Teams/TeamPages'; import TeamList from 'app/containers/Teams/TeamList'; @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..3ec82842934 --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,7 @@ +export interface FolderDTO { + id: number; + title: string; + url: string; + version: number; + hasAcl: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..e2d9cf8933f 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,3 +1,7 @@ +import { FolderDTO } from './dashboard'; + +export { FolderDTO }; + // // Location // From b1fe0c4c7e015b657fffe917638303950b2661c0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 15:53:58 +0200 Subject: [PATCH 10/28] team settings --- public/app/features/teams/TeamPages.tsx | 2 +- .../app/features/teams/TeamSettings.test.tsx | 44 ++++++++++++++ public/app/features/teams/TeamSettings.tsx | 59 ++++++++++++++----- .../__snapshots__/TeamPages.test.tsx.snap | 12 +--- .../__snapshots__/TeamSettings.test.tsx.snap | 57 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 14 +++++ 6 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2528c3c87b8..a4ab4a06d4d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -66,7 +66,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.Settings: - return ; + return ; case PageTypes.GroupSync: return isSyncEnabled && ; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx new file mode 100644 index 00000000000..2e40a0e3c44 --- /dev/null +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamSettings } from './TeamSettings'; +import { getMockTeam } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + team: getMockTeam(), + updateTeam: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamSettings; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should update team', () => { + const { instance } = setup(); + const mockEvent = { preventDefault: jest.fn() }; + + instance.setState({ + name: 'test11', + }); + + instance.onUpdate(mockEvent); + + expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com'); + }); +}); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 6e3c90d93f9..ef9a5ae0b70 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,41 +1,58 @@ import React from 'react'; -import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import { Label } from 'app/core/components/Forms/Forms'; import { Team } from '../../types'; +import { updateTeam } from './state/actions'; +import { getRouteParamsId } from '../../core/selectors/location'; +import { getTeam } from './state/selectors'; -interface Props { +export interface Props { team: Team; + updateTeam: typeof updateTeam; } -export class TeamSettings extends React.Component { +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { constructor(props) { super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; } - onChangeName = evt => { - // this.props.team.setName(evt.target.value); + onChangeName = event => { + this.setState({ name: event.target.value }); }; - onChangeEmail = evt => { - // this.props.team.setEmail(evt.target.value); + onChangeEmail = event => { + this.setState({ email: event.target.value }); }; - onUpdate = evt => { - evt.preventDefault(); - // this.props.team.update(); + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); }; render() { + const { name, email } = this.state; + return (

Team Settings

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

+ Team Settings +

+ +
+ + Name + + +
+
+ + Email + + +
+
+ +
+ +
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 4786edf60a8..5b203d0a502 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -153,6 +153,20 @@ export function removeTeamMember(id: number): ThunkResult { }; } +export function updateTeam(name: string, email: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + await getBackendSrv() + .put(`/api/teams/${team.id}`, { + name, + email, + }) + .then(() => { + dispatch(loadTeam(team.id)); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() From 0cfcf2685e66af76895664c86f562952d63ca812 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 16:58:17 +0200 Subject: [PATCH 11/28] actions for group sync --- .../app/features/teams/TeamGroupSync.test.tsx | 0 public/app/features/teams/TeamGroupSync.tsx | 81 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 3 +- public/app/features/teams/state/actions.ts | 68 +++++++++++++++- public/app/features/teams/state/reducers.ts | 3 + public/app/features/teams/state/selectors.ts | 1 + 6 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 6562820d717..39fdd8d413e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,11 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import { Team, TeamGroup } from '../../types'; +import { TeamGroup } from '../../types'; +import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; +import { getTeamGroups } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + groups: TeamGroup[]; + loadTeamGroups: typeof loadTeamGroups; + addTeamGroup: typeof addTeamGroup; + removeTeamGroup: typeof removeTeamGroup; } interface State { @@ -15,14 +20,39 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -export class TeamGroupSync extends React.Component { +export class TeamGroupSync extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newGroupId: '' }; } componentDidMount() { - // this.props.team.loadGroups(); + this.fetchTeamGroups(); + } + + async fetchTeamGroups() { + await this.props.loadTeamGroups(); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onNewGroupIdChanged = evt => { + this.setState({ newGroupId: evt.target.value }); + }; + + onAddGroup = () => { + this.props.addTeamGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: TeamGroup) => { + this.props.removeTeamGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; } renderGroup(group: TeamGroup) { @@ -38,30 +68,9 @@ export class TeamGroupSync extends React.Component { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onNewGroupIdChanged = evt => { - this.setState({ newGroupId: evt.target.value }); - }; - - onAddGroup = () => { - // this.props.team.addGroup(this.state.newGroupId); - this.setState({ isAdding: false, newGroupId: '' }); - }; - - onRemoveGroup = (group: TeamGroup) => { - // this.props.team.removeGroup(group.groupId); - }; - - isNewGroupValid() { - return this.state.newGroupId.length > 1; - } - render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups; + const groups = this.props.groups; return (
@@ -144,4 +153,16 @@ export class TeamGroupSync extends React.Component { } } -export default hot(module)(TeamGroupSync); +function mapStateToProps(state) { + return { + groups: getTeamGroups(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamGroups, + addTeamGroup, + removeTeamGroup, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index a4ab4a06d4d..f28bde518d2 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -57,7 +57,6 @@ export class TeamPages extends PureComponent { } renderPage() { - const { team } = this.props; const { isSyncEnabled } = this.state; const currentPage = this.getCurrentPage(); @@ -69,7 +68,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.GroupSync: - return isSyncEnabled && ; + return isSyncEnabled && ; } return null; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5b203d0a502..9b3ab3a8177 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,9 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; +import config from 'app/core/config'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -10,6 +11,7 @@ export enum ActionTypes { SetSearchQuery = 'SET_SEARCH_QUERY', SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', } export interface LoadTeamsAction { @@ -27,6 +29,11 @@ export interface LoadTeamMembersAction { payload: TeamMember[]; } +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; @@ -42,7 +49,8 @@ export type Action = | SetSearchQueryAction | LoadTeamAction | LoadTeamMembersAction - | SetSearchMemberQueryAction; + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; type ThunkResult = ThunkAction; @@ -61,6 +69,11 @@ const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => payload: teamMembers, }); +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ type: ActionTypes.SetSearchMemberQuery, payload: searchQuery, @@ -79,7 +92,7 @@ export function loadTeams(): ThunkResult { } function buildNavModel(team: Team): NavModelItem { - return { + const navModel = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', @@ -103,6 +116,18 @@ function buildNavModel(team: Team): NavModelItem { }, ], }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: 'team-settings', + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; } export function loadTeam(id: number): ThunkResult { @@ -117,7 +142,6 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { - console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; @@ -167,6 +191,42 @@ export function updateTeam(name: string, email: string): ThunkResult { }; } +export function loadTeamGroups(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/groups`) + .then(response => { + dispatch(teamGroupsLoaded(response)); + }); + }; +} + +export function addTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + +export function removeTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/groups/${groupId}`) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index f02ade60923..4af36f2e01c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -30,6 +30,9 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 5e22f96eaf7..416e293ec78 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,6 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; +export const getTeamGroups = state => state.groups; export const getTeam = (state, currentTeamId) => { if (state.team.id === parseInt(currentTeamId)) { From 679ffbfd8320490c20bb02acc1648557764734df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 21:49:04 +0200 Subject: [PATCH 12/28] wip: progress on redux folder store --- public/app/core/actions/index.ts | 4 +- .../manage-dashboards/state/actions.ts | 39 ++++++++++++++++++- .../manage-dashboards/state/reducers.ts | 20 ++++++++++ public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 12 +++++- public/app/types/index.ts | 5 ++- 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index b4b9b21126e..451a13dae99 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,4 @@ import { updateLocation } from './location'; -import { updateNavIndex } from './navModel'; +import { updateNavIndex, UpdateNavIndexAction } from './navModel'; -export { updateLocation, updateNavIndex }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction }; diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index ab5e1212d5f..b3243c7bf2b 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,7 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, NavModelItem } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -19,11 +20,45 @@ export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ export type Action = LoadFolderAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; +function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); }; } diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index e69de29bb2d..1eb873f5bd0 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -0,0 +1,20 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = null; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + canSave: false, + hasChanged: false, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..5aa5ccc5f41 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ 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'; +import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...manageDashboardsReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 3ec82842934..576432d413e 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,7 +1,17 @@ export interface FolderDTO { id: number; + uid: string; title: string; url: string; version: number; - hasAcl: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 221a64b48d4..bc54cea35cb 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,6 +1,6 @@ -import { FolderDTO } from './dashboard'; +import { FolderDTO, FolderState } from './dashboard'; -export { FolderDTO }; +export { FolderDTO, FolderState }; // // Location @@ -136,4 +136,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From 61112d93d8caa8a00b6ef2d1745d18706baf22f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 10:36:55 +0200 Subject: [PATCH 13/28] wip: folder to redux --- .../manage-dashboards/FolderSettingsPage.tsx | 60 +++++++++---------- .../manage-dashboards/state/reducers.ts | 8 ++- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 4ed6743a8dc..90528a8798d 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -120,48 +120,41 @@ export class FolderSettingsPage extends PureComponent { render() { const { navModel } = this.props; - // if (!folder.folder || !nav.main) { - // return

Loading

; - // } - return (

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
); } - - // asd() { - //
- //
- //
- // - // - //
- //
- // - // - //
- // - //
- // - // } } const mapStateToProps = (state: StoreState) => { @@ -170,6 +163,7 @@ const mapStateToProps = (state: StoreState) => { return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, + folder: state.folder, }; }; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 1eb873f5bd0..ee837acc9db 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -1,7 +1,13 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; -export const inititalState: FolderState = null; +export const inititalState: FolderState = { + uid: 'loading', + id: -1, + title: 'loading', + canSave: false, + hasChanged: false, +}; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { From 19cbff658bb53bc33ccfcaa84cc5d01fd7d76705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 17:36:23 +0200 Subject: [PATCH 14/28] wip: folder settings page to redux progress --- public/app/core/reducers/location.ts | 4 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ----------- .../manage-dashboards/FolderSettingsPage.tsx | 152 +++++------------- .../manage-dashboards/state/actions.ts | 24 ++- .../manage-dashboards/state/reducers.ts | 7 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/types/dashboard.ts | 1 + public/app/types/index.ts | 1 + 10 files changed, 72 insertions(+), 283 deletions(-) delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 90528a8798d..a23e495fd3c 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -4,121 +4,53 @@ import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState } from 'app/types'; -import { getFolderByUid } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; export interface Props { navModel: NavModel; folderUid: string; + folder: FolderState; getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; } export class FolderSettingsPage extends PureComponent { - // formSnapshot: any; - // componentDidMount() { this.props.getFolderByUid(this.props.folderUid); } - // - // loadStore() { - // const { nav, folder, view } = this.props; - // - // return folder.load(view.routeParams.get('uid') as string).then(res => { - // this.formSnapshot = getSnapshot(folder); - // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - // - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // } - // onTitleChange(evt) { - // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - // } - // - // getFormSnapshot() { - // if (!this.formSnapshot) { - // this.formSnapshot = getSnapshot(this.props.folder); - // } - // - // return this.formSnapshot; - // } - // - // save(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { nav, folder, view } = this.props; - // - // folder - // .saveFolder({ overwrite: false }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }) - // .catch(this.handleSaveFolderError.bind(this)); - // } - // - // delete(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { folder, view } = this.props; - // const title = folder.folder.title; - // - // appEvents.emit('confirm-modal', { - // title: 'Delete', - // text: `Do you want to delete this folder and all its dashboards?`, - // icon: 'fa-trash', - // yesText: 'Delete', - // onConfirm: () => { - // return folder.deleteFolder().then(() => { - // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - // view.updatePathAndQuery('dashboards', '', ''); - // }); - // }, - // }); - // } - // - // handleSaveFolderError(err) { - // if (err.data && err.data.status === 'version-mismatch') { - // err.isHandled = true; - // - // const { nav, folder, view } = this.props; - // - // appEvents.emit('confirm-modal', { - // title: 'Conflict', - // text: 'Someone else has updated this folder.', - // text2: 'Would you still like to save this folder?', - // yesText: 'Save & Overwrite', - // icon: 'fa-warning', - // onConfirm: () => { - // folder - // .saveFolder({ overwrite: true }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // }, - // }); - // } - // } + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + appEvents.emit('alert-success', ['Folder saved']); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; render() { - const { navModel } = this.props; + const { navModel, folder } = this.props; return (
@@ -127,25 +59,21 @@ export class FolderSettingsPage extends PureComponent {

Folder Settings

-
+
- -
@@ -159,7 +87,6 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; - return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, @@ -169,6 +96,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index acd5571aa95..84fe97f22ab 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,8 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, NavModelItem } from 'app/types'; -import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; +import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -32,7 +32,7 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ export type Action = LoadFolderAction | SetFolderTitleAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -67,6 +67,7 @@ function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); @@ -74,3 +75,20 @@ export function getFolderByUid(uid: string): ThunkResult { dispatch(updateNavIndex(buildNavModel(folder))); }; } + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 4844b465dfb..ada5b1812ad 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -16,9 +16,14 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat case ActionTypes.LoadFolder: return { ...action.payload, - canSave: false, hasChanged: false, }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: true, + }; } return state; }; diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 576432d413e..6fbe79cce8c 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -4,6 +4,7 @@ export interface FolderDTO { title: string; url: string; version: number; + canSave: boolean; } export interface FolderState { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 0ddb8f7cd0f..b1096c4827c 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -30,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From ec41d7608089ab65f78954a5218d7eef4bc578ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 19:00:37 +0200 Subject: [PATCH 15/28] mobx -> redux: major progress on folder migration --- public/app/containers/ContainerProps.ts | 2 - .../manage-dashboards}/FolderPermissions.tsx | 49 ++++--- .../FolderSettingsPage.test.tsx | 118 ++++++---------- .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++++++ .../manage-dashboards/state/reducers.ts | 4 +- public/app/routes/routes.ts | 2 +- public/app/stores/RootStore/RootStore.ts | 2 - public/app/types/{dashboard.ts => folder.ts} | 0 public/app/types/index.ts | 2 +- 9 files changed, 211 insertions(+), 99 deletions(-) rename public/app/{containers/ManageDashboards => features/manage-dashboards}/FolderPermissions.tsx (65%) create mode 100644 public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename public/app/types/{dashboard.ts => folder.ts} (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index ce09b992f80..84c395413b6 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,13 +1,11 @@ import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { nav: typeof NavStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; backendSrv: any; } diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/manage-dashboards/FolderPermissions.tsx similarity index 65% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/manage-dashboards/FolderPermissions.tsx index 072908d2b8e..00b229801f3 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/manage-dashboards/FolderPermissions.tsx @@ -2,24 +2,34 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +37,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { + if (folder.id === 0) { return

Loading

; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
- +

Folder Permissions

@@ -77,4 +79,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx index bed3d569bcc..defec5e6a57 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx @@ -1,84 +1,54 @@ import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel, FolderState } from '../../types'; import { shallow } from 'enzyme'; -describe('FolderSettings', () => { - let wrapper; - let page; +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', + uid: '1234', + title: 'loading', canSave: true, + hasChanged: true, version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); + }); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
+ +
+

+ Folder Settings +

+
+ +
+ + +
+
+ + +
+ +
+
+
+`; + +exports[`Render should render component 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index ada5b1812ad..41ae10d19e5 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -2,8 +2,8 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { + id: 0, uid: 'loading', - id: -1, title: 'loading', url: '', canSave: false, @@ -22,7 +22,7 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return { ...state, title: action.payload, - hasChanged: true, + hasChanged: action.payload.trim().length > 0, }; } return state; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 93d83a3b7db..45e72e68c38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ 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 FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; +import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/types/dashboard.ts b/public/app/types/folder.ts similarity index 100% rename from public/app/types/dashboard.ts rename to public/app/types/folder.ts diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b1096c4827c..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,7 @@ 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'; -import { FolderDTO, FolderState } from './dashboard'; +import { FolderDTO, FolderState } from './folder'; export { Team, From a83beac565e55f14089b26ed025ed5a7b66e2cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:15:18 +0200 Subject: [PATCH 16/28] redux: moved folders to it's own features folder --- public/app/containers/ContainerProps.ts | 12 --- .../FolderPermissions.tsx | 6 +- .../FolderSettingsPage.test.tsx | 3 +- .../FolderSettingsPage.tsx | 1 - .../FolderSettingsPage.test.tsx.snap | 0 .../state/actions.ts | 51 +++---------- public/app/features/folders/state/navModel.ts | 35 +++++++++ .../state/reducers.ts | 0 public/app/features/teams/state/actions.ts | 74 +++++-------------- public/app/routes/routes.ts | 4 +- public/app/stores/configureStore.ts | 4 +- yarn.lock | 30 +------- 12 files changed, 80 insertions(+), 140 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts rename public/app/features/{manage-dashboards => folders}/FolderPermissions.tsx (93%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.test.tsx (95%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.tsx (98%) rename public/app/features/{manage-dashboards => folders}/__snapshots__/FolderSettingsPage.test.tsx.snap (100%) rename public/app/features/{manage-dashboards => folders}/state/actions.ts (64%) create mode 100644 public/app/features/folders/state/navModel.ts rename public/app/features/{manage-dashboards => folders}/state/reducers.ts (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index 84c395413b6..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/features/manage-dashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 93% rename from public/app/features/manage-dashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 00b229801f3..1dc34aaba1e 100644 --- a/public/app/features/manage-dashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -11,13 +10,16 @@ import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; export interface Props { navModel: NavModel; getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; } @inject('permissions') diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx similarity index 95% rename from public/app/features/manage-dashboards/FolderSettingsPage.test.tsx rename to public/app/features/folders/FolderSettingsPage.test.tsx index defec5e6a57..3680fa9a197 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { FolderSettingsPage, Props } from './FolderSettingsPage'; -import { NavModel, FolderState } from '../../types'; +import { NavModel } from 'app/types'; import { shallow } from 'enzyme'; const setup = (propOverrides?: object) => { @@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => { uid: '1234', title: 'loading', canSave: true, + url: 'url', hasChanged: false, version: 1, }, diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx similarity index 98% rename from public/app/features/manage-dashboards/FolderSettingsPage.tsx rename to public/app/features/folders/FolderSettingsPage.tsx index a23e495fd3c..2aff0e3e1c4 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -31,7 +31,6 @@ export class FolderSettingsPage extends PureComponent { evt.stopPropagation(); await this.props.saveFolder(this.props.folder); - appEvents.emit('alert-success', ['Folder saved']); }; onDelete = evt => { diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap similarity index 100% rename from public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename to public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/folders/state/actions.ts similarity index 64% rename from public/app/features/manage-dashboards/state/actions.ts rename to public/app/features/folders/state/actions.ts index 84fe97f22ab..5d153b2fb8a 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,8 +1,10 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { FolderDTO, FolderState } from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -15,16 +17,16 @@ export interface LoadFolderAction { payload: FolderDTO; } -export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ - type: ActionTypes.LoadFolder, - payload: folder, -}); - export interface SetFolderTitleAction { type: ActionTypes.SetFolderTitle; payload: string; } +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ type: ActionTypes.SetFolderTitle, payload: newTitle, @@ -34,39 +36,6 @@ export type Action = LoadFolderAction | SetFolderTitleAction; type ThunkResult = ThunkAction; -function buildNavModel(folder: FolderDTO): NavModelItem { - return { - icon: 'fa fa-folder-open', - id: 'manage-folder', - subTitle: 'Manage folder dashboards & permissions', - url: '', - text: folder.title, - breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], - children: [ - { - active: false, - icon: 'fa fa-fw fa-th-large', - id: `folder-dashboards-${folder.uid}`, - text: 'Dashboards', - url: folder.url, - }, - { - active: false, - icon: 'fa fa-fw fa-lock', - id: `folder-permissions-${folder.uid}`, - text: 'Permissions', - url: `${folder.url}/permissions`, - }, - { - active: false, - icon: 'fa fa-fw fa-cog', - id: `folder-settings-${folder.uid}`, - text: 'Settings', - url: `${folder.url}/settings`, - }, - ], - }; -} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -82,6 +51,10 @@ export function saveFolder(folder: FolderState): ThunkResult { title: folder.title, version: folder.version, }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + dispatch(updateLocation({ path: `${res.url}/settings` })); }; } diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..614bb30f2d8 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,35 @@ +import { FolderDTO, NavModelItem } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/folders/state/reducers.ts similarity index 100% rename from public/app/features/manage-dashboards/state/reducers.ts rename to public/app/features/folders/state/reducers.ts diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..63bea743607 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -131,107 +131,71 @@ function buildNavModel(team: Team): NavModelItem { export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + 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)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + 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()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + 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()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + 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)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + 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)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + 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()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + 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()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 45e72e68c38..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; -import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 5aa5ccc5f41..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,13 +4,13 @@ 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'; -import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, - ...manageDashboardsReducers, + ...foldersReducers, }); export let store; diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From 0705bf570d403ed4233d4152b3e14a97b71cffc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:47:23 +0200 Subject: [PATCH 17/28] fix: added loading nav states --- public/app/core/selectors/navModel.ts | 10 ++- .../features/folders/FolderPermissions.tsx | 5 +- .../features/folders/FolderSettingsPage.tsx | 4 +- public/app/features/folders/state/navModel.ts | 20 +++++- public/app/features/teams/TeamPages.tsx | 8 ++- public/app/features/teams/state/actions.ts | 43 +----------- public/app/features/teams/state/navModel.ts | 67 +++++++++++++++++++ 7 files changed, 106 insertions(+), 51 deletions(-) create mode 100644 public/app/features/teams/state/navModel.ts diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 1dc34aaba1e..512927c24e6 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -12,6 +12,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid } from './state/actions'; import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -48,7 +49,7 @@ export class FolderPermissions extends Component { const { navModel, permissions, backendSrv, folder } = this.props; if (folder.id === 0) { - return

Loading

; + return ; } const dashboardId = folder.id; @@ -84,7 +85,7 @@ export class FolderPermissions extends Component { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; return { - navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 2aff0e3e1c4..1eb7ccafc65 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -6,6 +6,7 @@ import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -86,8 +87,9 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; + return { - navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 614bb30f2d8..e6ef763d019 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -1,4 +1,4 @@ -import { FolderDTO, NavModelItem } from 'app/types'; +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; export function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -33,3 +33,21 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ 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 { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 63bea743607..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,45 +90,6 @@ export function loadTeams(): ThunkResult { }; } -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 => { const response = await getBackendSrv().get(`/api/teams/${id}`); diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export 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 getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} From 78d36f784f3e17a0da13ba0ab007e287eb6f3034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:55:22 +0200 Subject: [PATCH 18/28] fix: gofmt issues --- pkg/models/datasource.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b7e3e3eaa17..cbdd0136f4d 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -59,22 +59,22 @@ type DataSource struct { } var knownDatasourcePlugins = map[string]bool{ - DS_ES: true, - DS_GRAPHITE: true, - DS_INFLUXDB: true, - DS_INFLUXDB_08: true, - DS_KAIROSDB: true, - DS_CLOUDWATCH: true, - DS_PROMETHEUS: true, - DS_OPENTSDB: true, - DS_POSTGRES: true, - DS_MYSQL: true, - DS_MSSQL: true, - "opennms": true, - "abhisant-druid-datasource": true, - "dalmatinerdb-datasource": true, - "gnocci": true, - "zabbix": true, + DS_ES: true, + DS_GRAPHITE: true, + DS_INFLUXDB: true, + DS_INFLUXDB_08: true, + DS_KAIROSDB: true, + DS_CLOUDWATCH: true, + DS_PROMETHEUS: true, + DS_OPENTSDB: true, + DS_POSTGRES: true, + DS_MYSQL: true, + DS_MSSQL: true, + "opennms": true, + "abhisant-druid-datasource": true, + "dalmatinerdb-datasource": true, + "gnocci": true, + "zabbix": true, "alexanderzobnin-zabbix-datasource": true, "newrelic-app": true, "grafana-datadog-datasource": true, From a317158b72c7841dcef935452ecc7a316eb7c8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 12:18:24 +0200 Subject: [PATCH 19/28] wip: working on reducer test --- .../app/features/folders/state/reducers.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 public/app/features/folders/state/reducers.test.ts diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..1a7f4310f76 --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,17 @@ +import { Action, ActionTypes } from './actions'; +import { inititalState, folderReducer } from './reducers'; + +describe('folder 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); + }); +}); From 8096cd8f3374a174e113ffe53276af0acd8cf434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 07:30:27 +0200 Subject: [PATCH 20/28] fix: added reducer test --- .../features/folders/state/reducers.test.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index 1a7f4310f76..ff37f13f97f 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,17 +1,42 @@ import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; import { inititalState, folderReducer } from './reducers'; +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + describe('folder reducer', () => { - it('should set teams', () => { - const payload = [getMockTeam()]; + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); const action: Action = { - type: ActionTypes.LoadTeams, - payload, + type: ActionTypes.LoadFolder, + payload: folder, }; - const result = teamsReducer(initialTeamsState, action); + const state = folderReducer(inititalState, action); - expect(result.teams).toEqual(payload); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); }); }); From f360b6186b0d0726762382caec2a787c493cb386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 10:52:29 +0200 Subject: [PATCH 21/28] wip: first couple of things starting to work --- public/app/core/actions/permissions.ts | 24 +++++ .../DisabledPermissionListItem.tsx | 43 ++++++++ .../PermissionList/PermissionList.tsx | 61 +++++++++++ .../PermissionList/PermissionListItem.tsx | 100 ++++++++++++++++++ .../features/folders/FolderPermissions.tsx | 70 +++++++----- public/app/features/folders/state/actions.ts | 85 ++++++++++++++- public/app/features/folders/state/reducers.ts | 41 ++++++- public/app/types/acl.ts | 60 +++++++++++ public/app/types/folder.ts | 11 +- public/app/types/index.ts | 8 +- 10 files changed, 465 insertions(+), 38 deletions(-) create mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/components/PermissionList/DisabledPermissionListItem.tsx create mode 100644 public/app/core/components/PermissionList/PermissionList.tsx create mode 100644 public/app/core/components/PermissionList/PermissionListItem.tsx create mode 100644 public/app/types/acl.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts new file mode 100644 index 00000000000..2b07b7145dd --- /dev/null +++ b/public/app/core/actions/permissions.ts @@ -0,0 +1,24 @@ +import { DashboardAcl } from '../../types'; + +export enum ActionTypes { + LoadFolderPermissions = 'LoadFolderPermissions', +} + +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderPermissions; + +export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx new file mode 100644 index 00000000000..d65595dae66 --- /dev/null +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; + +export interface Props { + item: any; +} + +export default class DisabledPermissionListItem extends Component { + render() { + const { item } = this.props; + + return ( + + + + + + {item.name} + (Role) + + + Can + +
+ {}} + value={item.permission} + disabled={true} + className={'gf-form-input--form-dropdown-right'} + /> +
+ + + + + + ); + } +} diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx new file mode 100644 index 00000000000..29f810a4358 --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import PermissionsListItem from './PermissionListItem'; +import DisabledPermissionsListItem from './DisabledPermissionListItem'; +import { DashboardAcl, FolderInfo } from 'app/types'; + +export interface Props { + items: DashboardAcl[]; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: any; + isFetching: boolean; + folderInfo?: FolderInfo; +} + +class PermissionList extends PureComponent { + render() { + const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props; + + return ( + + + + {items.map((item, idx) => { + return ( + + ); + })} + {isFetching === true && items.length < 1 ? ( + + + + ) : null} + + {isFetching === false && items.length < 1 ? ( + + + + ) : null} + +
+ Loading permissions... +
+ No permissions are set. Will only be accessible by admins. +
+ ); + } +} + +export default PermissionList; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx new file mode 100644 index 00000000000..3e5aaf3ab2f --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -0,0 +1,100 @@ +import React, { PureComponent } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { dashboardPermissionLevels } from 'app/types/acl'; +import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; + +const setClassNameHelper = inherited => { + return inherited ? 'gf-form-disabled' : ''; +}; + +function ItemAvatar({ item }) { + if (item.userAvatarUrl) { + return ; + } + if (item.teamAvatarUrl) { + return ; + } + if (item.role === 'Editor') { + return ; + } + + return ; +} + +function ItemDescription({ item }) { + if (item.userId) { + return (User); + } + if (item.teamId) { + return (Team); + } + return (Role); +} + +interface Props { + item: DashboardAcl; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void; + folderInfo?: FolderInfo; +} + +export default class PermissionsListItem extends PureComponent { + onPermissionChanged = option => { + this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel); + }; + + onRemoveItem = () => { + this.props.onRemoveItem(this.props.item); + }; + + render() { + const { item, folderInfo } = this.props; + const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; + + return ( + + + + + + {item.name} + + + {item.inherited && + folderInfo && ( + + Inherited from folder{' '} + + {folderInfo.title} + {' '} + + )} + {inheritedFromRoot && Default Permission} + + Can + +
+ +
+ + + {!item.inherited ? ( + + + + ) : ( + + )} + + + ); + } +} diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 512927c24e6..25de5f8be16 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -9,50 +8,61 @@ import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid } from './state/actions'; -import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; +import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; import { getLoadingNav } from './state/navModel'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; export interface Props { navModel: NavModel; - getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; - permissions: typeof PermissionsStore.Type; - backendSrv: any; + getFolderByUid: typeof getFolderByUid; + getFolderPermissions: typeof getFolderPermissions; + updateFolderPermission: typeof updateFolderPermission; + removeFolderPermission: typeof removeFolderPermission; } -@inject('permissions') -@observer -export class FolderPermissions extends Component { +export interface State { + isAdding: boolean; +} + +export class FolderPermissions extends Component { constructor(props) { super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); + + this.state = { + isAdding: false, + }; } componentDidMount() { this.props.getFolderByUid(this.props.folderUid); + this.props.getFolderPermissions(this.props.folderUid); } - componentWillUnmount() { - const { permissions } = this.props; - permissions.hideAddPermissions(); - } + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; - handleAddPermission() { - const { permissions } = this.props; - permissions.toggleAddPermissions(); - } + onRemoveItem = (item: DashboardAcl) => { + this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + this.props.updateFolderPermission(item, level); + }; render() { - const { navModel, permissions, backendSrv, folder } = this.props; + const { navModel, folder } = this.props; + const { isAdding } = this.state; if (folder.id === 0) { return ; } const dashboardId = folder.id; + const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; return (
@@ -64,18 +74,17 @@ export class FolderPermissions extends Component {
-
- - - - +
); @@ -93,6 +102,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 5d153b2fb8a..29940cc7a31 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,7 +1,14 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState } from 'app/types'; +import { + FolderDTO, + FolderState, + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, +} from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -10,6 +17,7 @@ export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', SetFolderTitle = 'SET_FOLDER_TITLE', SaveFolder = 'SAVE_FOLDER', + LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS', } export interface LoadFolderAction { @@ -22,6 +30,15 @@ export interface SetFolderTitleAction { payload: string; } +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction; + +type ThunkResult = ThunkAction; + export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ type: ActionTypes.LoadFolder, payload: folder, @@ -32,10 +49,10 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ payload: newTitle, }); -export type Action = LoadFolderAction | SetFolderTitleAction; - -type ThunkResult = ThunkAction; - +export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -65,3 +82,61 @@ export function deleteFolder(uid: string): ThunkResult { dispatch(updateLocation({ path: `dashboards` })); }; } + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateFolderPermission(itemToUpdate: DashboardAcl, level: PermissionLevel): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} + +export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 41ae10d19e5..6e6a671685a 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,4 @@ -import { FolderState } from 'app/types'; +import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { @@ -8,13 +8,15 @@ export const inititalState: FolderState = { url: '', canSave: false, hasChanged: false, - version: 0, + version: 1, + permissions: [], }; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { case ActionTypes.LoadFolder: return { + ...state, ...action.payload, hasChanged: false, }; @@ -24,10 +26,45 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat title: action.payload, hasChanged: action.payload.trim().length > 0, }; + case ActionTypes.LoadFolderPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; } return state; }; +function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} + export default { folder: folderReducer, }; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts new file mode 100644 index 00000000000..d77fc4793fc --- /dev/null +++ b/public/app/types/acl.ts @@ -0,0 +1,60 @@ +export interface DashboardAclDTO { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + inherited?: boolean; +} + +export interface DashboardAclUpdateDTO { + userId: number; + teamId: number; + role: string; + permission: PermissionLevel; +} + +export interface DashboardAcl { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + name?: string; + inherited?: boolean; + sortRank?: number; +} + +export interface DashboardPermissionInfo { + value: PermissionLevel; + label: string; + description: string; +} + +export enum PermissionLevel { + View = 1, + Edit = 2, + Admin = 4, +} + +export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ + { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, + { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, + { + value: PermissionLevel.Admin, + label: 'Admin', + description: 'Can add/remove permissions and can add, edit and delete dashboards.', + }, +]; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts index 6fbe79cce8c..bbcae01fe59 100644 --- a/public/app/types/folder.ts +++ b/public/app/types/folder.ts @@ -1,3 +1,5 @@ +import { DashboardAcl } from './acl'; + export interface FolderDTO { id: number; uid: string; @@ -12,7 +14,14 @@ export interface FolderState { uid: string; title: string; url: string; - version: number; canSave: boolean; hasChanged: boolean; + version: number; + permissions: DashboardAcl[]; +} + +export interface FolderInfo { + id: number; + title: string; + url: string; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 52d1ba592c5..49f7fdb0f28 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,8 @@ 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'; -import { FolderDTO, FolderState } from './folder'; +import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -22,6 +23,11 @@ export { UrlQueryValue, FolderDTO, FolderState, + FolderInfo, + DashboardAcl, + DashboardAclDTO, + DashboardAclUpdateDTO, + PermissionLevel, }; export interface StoreState { From d35eca333feb144d841693552842848402973644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 13:56:09 +0200 Subject: [PATCH 22/28] folder permissions in redux --- .../PermissionList/AddPermission.tsx | 142 ++++++++++++++++++ .../PermissionList/PermissionList.tsx | 3 +- .../PermissionList/PermissionListItem.tsx | 4 +- .../features/folders/FolderPermissions.tsx | 36 +++-- .../folders/FolderSettingsPage.test.tsx | 1 + public/app/features/folders/state/actions.ts | 31 +++- public/app/features/folders/state/reducers.ts | 3 +- public/app/types/acl.ts | 27 ++++ public/app/types/index.ts | 5 - 9 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 public/app/core/components/PermissionList/AddPermission.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx new file mode 100644 index 00000000000..76bcfac4780 --- /dev/null +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -0,0 +1,142 @@ +import React, { Component } from 'react'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; +import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; +import { + dashboardPermissionLevels, + dashboardAclTargets, + AclTarget, + PermissionLevel, + NewDashboardAclItem, +} from 'app/types/acl'; + +export interface Props { + onAddPermission: (item: NewDashboardAclItem) => void; + onCancel: () => void; +} + +class AddPermissions extends Component { + constructor(props) { + super(props); + this.state = this.getCleanState(); + } + + getCleanState() { + return { + userId: 0, + teamId: 0, + role: '', + type: AclTarget.Team, + permission: PermissionLevel.View, + }; + } + + onTypeChanged = evt => { + this.setState({ type: evt.target.value as AclTarget }); + }; + + onUserSelected = (user: User) => { + this.setState({ + userId: user ? user.id : 0, + teamId: 0, + }); + }; + + onTeamSelected = (team: Team) => { + this.setState({ + userId: 0, + teamId: team ? team.id : 0, + }); + }; + + onPermissionChanged = (permission: OptionWithDescription) => { + this.setState({ permission: permission.value }); + }; + + onSubmit = async evt => { + evt.preventDefault(); + await this.props.onAddPermission(this.state); + this.setState(this.getCleanState()); + }; + + isValid() { + switch (this.state.type) { + case AclTarget.Team: + return this.state.teamId > 0; + case AclTarget.User: + return this.state.userId > 0; + } + return true; + } + + render() { + const { onCancel } = this.props; + const newItem = this.state; + const pickerClassName = 'width-20'; + const isValid = this.isValid(); + + return ( +
+ +
+
Add Permission For
+
+
+
+ +
+
+ + {newItem.type === AclTarget.User ? ( +
+ +
+ ) : null} + + {newItem.type === AclTarget.Team ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+
+
+
+ ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx index 29f810a4358..772baa0c274 100644 --- a/public/app/core/components/PermissionList/PermissionList.tsx +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; import PermissionsListItem from './PermissionListItem'; import DisabledPermissionsListItem from './DisabledPermissionListItem'; -import { DashboardAcl, FolderInfo } from 'app/types'; +import { FolderInfo } from 'app/types'; +import { DashboardAcl } from 'app/types/acl'; export interface Props { items: DashboardAcl[]; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index 3e5aaf3ab2f..b846f98a063 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { dashboardPermissionLevels } from 'app/types/acl'; -import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; +import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; +import { FolderInfo } from 'app/types'; const setClassNameHelper = inherited => { return inherited ? 'gf-form-disabled' : ''; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 25de5f8be16..c86137a55ce 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,17 +1,23 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; -import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { + getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, + addFolderPermission, +} from './state/actions'; import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; export interface Props { navModel: NavModel; @@ -21,13 +27,14 @@ export interface Props { getFolderPermissions: typeof getFolderPermissions; updateFolderPermission: typeof updateFolderPermission; removeFolderPermission: typeof removeFolderPermission; + addFolderPermission: typeof addFolderPermission; } export interface State { isAdding: boolean; } -export class FolderPermissions extends Component { +export class FolderPermissions extends PureComponent { constructor(props) { super(props); @@ -53,6 +60,14 @@ export class FolderPermissions extends Component { this.props.updateFolderPermission(item, level); }; + onAddPermission = (newItem: NewDashboardAclItem) => { + return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + render() { const { navModel, folder } = this.props; const { isAdding } = this.state; @@ -61,8 +76,7 @@ export class FolderPermissions extends Component { return ; } - const dashboardId = folder.id; - const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; + const folderInfo = { title: folder.title, url: folder.url, id: folder.id }; return (
@@ -78,6 +92,9 @@ export class FolderPermissions extends Component { Add Permission
+ + + { url: 'url', hasChanged: false, version: 1, + permissions: [], }, getFolderByUid: jest.fn(), setFolderTitle: jest.fn(), diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 29940cc7a31..4f15f813a68 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,14 +1,15 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; import { - FolderDTO, - FolderState, DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO, -} from 'app/types'; + NewDashboardAclItem, +} from 'app/types/acl'; + import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -140,3 +141,27 @@ export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult< await dispatch(getFolderPermissions(folder.uid)); }; } + +export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: item.role, + permission: item.permission, + }); + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 6e6a671685a..9b73312790c 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,5 @@ -import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; +import { FolderState } from 'app/types'; +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d77fc4793fc..feca062b355 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -43,12 +43,39 @@ export interface DashboardPermissionInfo { description: string; } +export interface NewDashboardAclItem { + teamId: number; + userId: number; + role: string; + permission: PermissionLevel; + type: AclTarget; +} + export enum PermissionLevel { View = 1, Edit = 2, Admin = 4, } +export enum AclTarget { + Team = 'team', + User = 'user', + Viewer = 'viewer', + Editor = 'editor', +} + +export interface AclTargetInfo { + value: AclTarget; + text: string; +} + +export const dashboardAclTargets: AclTargetInfo[] = [ + { value: AclTarget.Team, text: 'Team' }, + { value: AclTarget.User, text: 'User' }, + { value: AclTarget.Viewer, text: 'Everyone With Viewer Role' }, + { value: AclTarget.Editor, text: 'Everyone With Editor Role' }, +]; + export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 49f7fdb0f28..6f052c7c503 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,7 +3,6 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; -import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -24,10 +23,6 @@ export { FolderDTO, FolderState, FolderInfo, - DashboardAcl, - DashboardAclDTO, - DashboardAclUpdateDTO, - PermissionLevel, }; export interface StoreState { From e1a1da9064b7ffd7b22764a99a800b04f7cbc747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:31:12 +0200 Subject: [PATCH 23/28] fix: add folder permission fix --- public/app/features/folders/state/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 4f15f813a68..df81acb91eb 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -157,8 +157,8 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult Date: Thu, 13 Sep 2018 16:00:02 +0200 Subject: [PATCH 24/28] wip: dashboard permissions to redux --- public/app/core/actions/permissions.ts | 24 ---- public/app/core/angular_wrappers.ts | 2 - public/app/core/reducers/processsAclItems.ts | 31 +++++ public/app/core/utils/acl.ts | 31 +++++ .../DashboardPermissions.tsx | 106 ++++++++++++++++ public/app/features/dashboard/all.ts | 6 + .../app/features/dashboard/state/actions.ts | 115 ++++++++++++++++++ .../app/features/dashboard/state/reducers.ts | 22 ++++ public/app/features/folders/state/reducers.ts | 32 +---- public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 5 + public/app/types/index.ts | 2 + 12 files changed, 321 insertions(+), 57 deletions(-) delete mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/reducers/processsAclItems.ts create mode 100644 public/app/core/utils/acl.ts create mode 100644 public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx create mode 100644 public/app/features/dashboard/state/actions.ts create mode 100644 public/app/features/dashboard/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts deleted file mode 100644 index 2b07b7145dd..00000000000 --- a/public/app/core/actions/permissions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DashboardAcl } from '../../types'; - -export enum ActionTypes { - LoadFolderPermissions = 'LoadFolderPermissions', -} - -export interface LoadFolderPermissionsAction { - type: ActionTypes.LoadFolderPermissions; - payload: DashboardAcl[]; -} - -export type Action = LoadFolderPermissions; - -export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ - type: ActionTypes.LoadFolderPermissions, - payload: items, -}); - -export function getFolderPermissions(uid: string): ThunkResult { - return async dispatch => { - const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); - dispatch(loadFolderPermissions(permissions)); - }; -} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 18e9d8dbd84..6974d40aac8 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; -import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -18,5 +17,4 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/reducers/processsAclItems.ts b/public/app/core/reducers/processsAclItems.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/reducers/processsAclItems.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/core/utils/acl.ts b/public/app/core/utils/acl.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/utils/acl.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx new file mode 100644 index 00000000000..ad7d9c7f504 --- /dev/null +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -0,0 +1,106 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import SlideDown from 'app/core/components/Animations/SlideDown'; +import { StoreState, FolderInfo } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { getDashboardPermissions } from '../state/actions'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import { store } from 'app/stores/configureStore'; + +export interface Props { + dashboardId: number; + folder?: FolderInfo; + getDashboardPermissions: typeof getDashboardPermissions; + permissions: DashboardAcl[]; +} + +export interface State { + isAdding: boolean; +} + +export class DashboardPermissions extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isAdding: false, + }; + } + + componentDidMount() { + this.props.getDashboardPermissions(this.props.dashboardId); + } + + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; + + onRemoveItem = (item: DashboardAcl) => { + // this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + // this.props.updateFolderPermission(item, level); + }; + + onAddPermission = (newItem: NewDashboardAclItem) => { + // return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + + render() { + const { permissions, folder } = this.props; + const { isAdding } = this.state; + console.log('DashboardPermissions', this.props); + + return ( +
+
+
+

Permissions

+ + + +
+ +
+
+ + + + +
+ ); + } +} + +function connectWithStore(WrappedComponent, ...args) { + const ConnectedWrappedComponent = connect(...args)(WrappedComponent); + return props => { + return ; + }; +} + +const mapStateToProps = (state: StoreState) => ({ + permissions: state.dashboard.permissions, +}); + +const mapDispatchToProps = { + getDashboardPermissions, +}; + +export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index adb665c47b5..817ed83aaa0 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -30,6 +30,12 @@ import './settings/settings'; import './panellinks/module'; import './dashlinks/module'; +// angular wrappers +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import DashboardPermissions from './DashboardPermissions/DashboardPermissions'; + +react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']); + import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts new file mode 100644 index 00000000000..b1d25d1f57f --- /dev/null +++ b/public/app/features/dashboard/state/actions.ts @@ -0,0 +1,115 @@ +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; + +import { + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, + NewDashboardAclItem, +} from 'app/types/acl'; + +export enum ActionTypes { + LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS', +} + +export interface LoadDashboardPermissionsAction { + type: ActionTypes.LoadDashboardPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadDashboardPermissionsAction; + +type ThunkResult = ThunkAction; + +export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({ + type: ActionTypes.LoadDashboardPermissions, + payload: items, +}); + +export function getDashboardPermissions(id: number): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/dashboards/id/${id}/permissions`); + dispatch(loadDashboardPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateDashboardPermission( + dashboardId: number, + itemToUpdate: DashboardAcl, + level: PermissionLevel +): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function removeDashboardPermission(dashboardId: number, itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const dashboard = getStore().dashboard; + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function addDashboardPermission(dashboardId: number, newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: newItem.role, + permission: newItem.permission, + }); + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts new file mode 100644 index 00000000000..5100529d973 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.ts @@ -0,0 +1,22 @@ +import { DashboardState } from 'app/types'; +import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; + +export const inititalState: DashboardState = { + permissions: [], +}; + +export const dashboardReducer = (state = inititalState, action: Action): DashboardState => { + switch (action.type) { + case ActionTypes.LoadDashboardPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; + } + return state; +}; + +export default { + dashboard: dashboardReducer, +}; diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 9b73312790c..4560c999659 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,6 +1,6 @@ import { FolderState } from 'app/types'; -import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; export const inititalState: FolderState = { id: 0, @@ -36,36 +36,6 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return state; }; -function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { - return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); -} - -function processAclItem(dto: DashboardAclDTO): DashboardAcl { - const item = dto as DashboardAcl; - - item.sortRank = 0; - if (item.userId > 0) { - item.name = item.userLogin; - item.sortRank = 10; - } else if (item.teamId > 0) { - item.name = item.team; - item.sortRank = 20; - } else if (item.role) { - item.icon = 'fa fa-fw fa-street-view'; - item.name = item.role; - item.sortRank = 30; - if (item.role === 'Editor') { - item.sortRank += 1; - } - } - - if (item.inherited) { - item.sortRank += 100; - } - - return item; -} - export default { folder: folderReducer, }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index e06317853f8..8f6cf25043d 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -5,12 +5,14 @@ import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; +import dashboardReducers from 'app/features/dashboard/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, ...foldersReducers, + ...dashboardReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..d33405c985e --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,5 @@ +import { DashboardAcl } from './acl'; + +export interface DashboardState { + permissions: DashboardAcl[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 6f052c7c503..f2fe165a863 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,6 +3,7 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardState } from './dashboard'; export { Team, @@ -32,4 +33,5 @@ export interface StoreState { teams: TeamsState; team: TeamState; folder: FolderState; + dashboard: DashboardState; } From 7bb010926196cc9c5f25a0c850baac13834d89c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:47:33 +0200 Subject: [PATCH 25/28] feat: dashboard permissions are working --- .../PermissionList/AddPermission.tsx | 3 +- .../PermissionsInfo.tsx | 0 .../Permissions/AddPermissions.test.tsx | 90 ------------ .../components/Permissions/AddPermissions.tsx | 128 ------------------ .../Permissions/DashboardPermissions.tsx | 71 ---------- .../DisabledPermissionsListItem.tsx | 43 ------ .../core/components/Permissions/FolderInfo.ts | 5 - .../components/Permissions/Permissions.tsx | 91 ------------- .../Permissions/PermissionsList.tsx | 64 --------- .../Permissions/PermissionsListItem.tsx | 91 ------------- .../DashboardPermissions.tsx | 23 +++- .../app/features/dashboard/state/actions.ts | 2 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/state/reducers.test.ts | 92 ++++++++++--- public/app/types/acl.ts | 24 ++-- public/app/types/index.ts | 5 + scripts/webpack/webpack.common.js | 3 + 17 files changed, 117 insertions(+), 620 deletions(-) rename public/app/core/components/{Permissions => PermissionList}/PermissionsInfo.tsx (100%) delete mode 100644 public/app/core/components/Permissions/AddPermissions.test.tsx delete mode 100644 public/app/core/components/Permissions/AddPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DashboardPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DisabledPermissionsListItem.tsx delete mode 100644 public/app/core/components/Permissions/FolderInfo.ts delete mode 100644 public/app/core/components/Permissions/Permissions.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsList.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsListItem.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 76bcfac4780..73bffdaf97b 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -8,6 +8,7 @@ import { AclTarget, PermissionLevel, NewDashboardAclItem, + OrgRole, } from 'app/types/acl'; export interface Props { @@ -25,7 +26,7 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: '', + role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; diff --git a/public/app/core/components/Permissions/PermissionsInfo.tsx b/public/app/core/components/PermissionList/PermissionsInfo.tsx similarity index 100% rename from public/app/core/components/Permissions/PermissionsInfo.tsx rename to public/app/core/components/PermissionList/PermissionsInfo.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.test.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx deleted file mode 100644 index c6d1ab381b8..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import AddPermissions from './AddPermissions'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { getBackendSrv } from 'app/core/services/backend_srv'; - -jest.mock('app/core/services/backend_srv', () => ({ - getBackendSrv: () => { - return { - get: () => { - return Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - ]); - }, - post: jest.fn(() => Promise.resolve({})), - }; - }, -})); - -describe('AddPermissions', () => { - let wrapper; - let store; - let instance; - const backendSrv: any = getBackendSrv(); - - beforeAll(() => { - store = RootStore.create({}, { backendSrv: backendSrv }); - wrapper = shallow(); - instance = wrapper.instance(); - return store.permissions.load(1, true, false); - }); - - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'User', - }, - }; - const userItem = { - id: 2, - login: 'user2', - }; - - instance.onTypeChanged(evt); - instance.onUserSelected(userItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'Group', - }, - }; - - const teamItem = { - id: 2, - name: 'ug1', - }; - - instance.onTypeChanged(evt); - instance.onTeamSelected(teamItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - afterEach(() => { - backendSrv.post.mockClear(); - }); -}); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx deleted file mode 100644 index 289e27aa731..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; -import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; -import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; -import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - permissions: any; -} - -@observer -class AddPermissions extends Component { - constructor(props) { - super(props); - } - - componentWillMount() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - onTypeChanged = evt => { - const { value } = evt.target; - const { permissions } = this.props; - - permissions.setNewType(value); - }; - - onUserSelected = (user: User) => { - const { permissions } = this.props; - if (!user) { - permissions.newItem.setUser(null, null); - return; - } - return permissions.newItem.setUser(user.id, user.login, user.avatarUrl); - }; - - onTeamSelected = (team: Team) => { - const { permissions } = this.props; - if (!team) { - permissions.newItem.setTeam(null, null); - return; - } - return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl); - }; - - onPermissionChanged = (permission: OptionWithDescription) => { - const { permissions } = this.props; - return permissions.newItem.setPermission(permission.value); - }; - - resetNewType() { - const { permissions } = this.props; - return permissions.resetNewType(); - } - - onSubmit = evt => { - evt.preventDefault(); - const { permissions } = this.props; - permissions.addStoreItem(); - }; - - render() { - const { permissions } = this.props; - const newItem = permissions.newItem; - const pickerClassName = 'width-20'; - - const isValid = newItem.isValid(); - - return ( -
- -
-
Add Permission For
-
-
-
- -
-
- - {newItem.type === 'User' ? ( -
- -
- ) : null} - - {newItem.type === 'Group' ? ( -
- -
- ) : null} - -
- -
- -
- -
-
-
-
- ); - } -} - -export default AddPermissions; diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx deleted file mode 100644 index 38a646b2473..00000000000 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; -import Permissions from 'app/core/components/Permissions/Permissions'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; -import SlideDown from 'app/core/components/Animations/SlideDown'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - dashboardId: number; - folder?: FolderInfo; - backendSrv: any; -} - -@observer -class DashboardPermissions extends Component { - permissions: any; - - constructor(props) { - super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); - this.permissions = store.permissions; - } - - handleAddPermission() { - this.permissions.toggleAddPermissions(); - } - - componentWillUnmount() { - this.permissions.hideAddPermissions(); - } - - render() { - const { dashboardId, folder, backendSrv } = this.props; - - return ( -
-
-
-

Permissions

- - - -
- -
-
- - - - -
- ); - } -} - -export default DashboardPermissions; diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx deleted file mode 100644 index d65595dae66..00000000000 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - item: any; -} - -export default class DisabledPermissionListItem extends Component { - render() { - const { item } = this.props; - - return ( - - - - - - {item.name} - (Role) - - - Can - -
- {}} - value={item.permission} - disabled={true} - className={'gf-form-input--form-dropdown-right'} - /> -
- - - - - - ); - } -} diff --git a/public/app/core/components/Permissions/FolderInfo.ts b/public/app/core/components/Permissions/FolderInfo.ts deleted file mode 100644 index d4a6020bb71..00000000000 --- a/public/app/core/components/Permissions/FolderInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface FolderInfo { - id: number; - title: string; - url: string; -} diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx deleted file mode 100644 index d17899c891f..00000000000 --- a/public/app/core/components/Permissions/Permissions.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsList from './PermissionsList'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface DashboardAcl { - id?: number; - dashboardId?: number; - userId?: number; - userLogin?: string; - userEmail?: string; - teamId?: number; - team?: string; - permission?: number; - permissionName?: string; - role?: string; - icon?: string; - name?: string; - inherited?: boolean; - sortRank?: number; -} - -export interface Props { - dashboardId: number; - folderInfo?: FolderInfo; - permissions?: any; - isFolder: boolean; - backendSrv: any; -} - -@observer -class Permissions extends Component { - constructor(props) { - super(props); - const { dashboardId, isFolder, folderInfo } = this.props; - this.permissionChanged = this.permissionChanged.bind(this); - this.typeChanged = this.typeChanged.bind(this); - this.removeItem = this.removeItem.bind(this); - this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0); - } - - loadStore(dashboardId, isFolder, isInRoot = false) { - return this.props.permissions.load(dashboardId, isFolder, isInRoot); - } - - permissionChanged(index: number, permission: number, permissionName: string) { - const { permissions } = this.props; - permissions.updatePermissionOnIndex(index, permission, permissionName); - } - - removeItem(index: number) { - const { permissions } = this.props; - permissions.removeStoreItem(index); - } - - resetNewType() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - typeChanged(evt) { - const { value } = evt.target; - const { permissions, dashboardId } = this.props; - - if (value === 'Viewer' || value === 'Editor') { - permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); - this.resetNewType(); - return; - } - - permissions.setNewType(value); - } - - render() { - const { permissions, folderInfo } = this.props; - - return ( -
- -
- ); - } -} - -export default Permissions; diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx deleted file mode 100644 index 7e64de012e4..00000000000 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsListItem from './PermissionsListItem'; -import DisabledPermissionsListItem from './DisabledPermissionsListItem'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - permissions: any[]; - removeItem: any; - permissionChanged: any; - fetching: boolean; - folderInfo?: FolderInfo; -} - -@observer -class PermissionsList extends Component { - render() { - const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props; - - return ( - - - - {permissions.map((item, idx) => { - return ( - - ); - })} - {fetching === true && permissions.length < 1 ? ( - - - - ) : null} - - {fetching === false && permissions.length < 1 ? ( - - - - ) : null} - -
- Loading permissions... -
- No permissions are set. Will only be accessible by admins. -
- ); - } -} - -export default PermissionsList; diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx deleted file mode 100644 index a17aa8c04df..00000000000 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -const setClassNameHelper = inherited => { - return inherited ? 'gf-form-disabled' : ''; -}; - -function ItemAvatar({ item }) { - if (item.userAvatarUrl) { - return ; - } - if (item.teamAvatarUrl) { - return ; - } - if (item.role === 'Editor') { - return ; - } - - return ; -} - -function ItemDescription({ item }) { - if (item.userId) { - return (User); - } - if (item.teamId) { - return (Team); - } - return (Role); -} - -export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => { - const handleRemoveItem = evt => { - evt.preventDefault(); - removeItem(itemIndex); - }; - - const handleChangePermission = permissionOption => { - permissionChanged(itemIndex, permissionOption.value, permissionOption.label); - }; - - const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; - - return ( - - - - - - {item.name} - - - {item.inherited && - folderInfo && ( - - Inherited from folder{' '} - - {folderInfo.title} - {' '} - - )} - {inheritedFromRoot && Default Permission} - - Can - -
- -
- - - {!item.inherited ? ( - - - - ) : ( - - )} - - - ); -}); diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx index ad7d9c7f504..6ea7ba12721 100644 --- a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -4,17 +4,25 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; -import { getDashboardPermissions } from '../state/actions'; +import { + getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, +} from '../state/actions'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import { store } from 'app/stores/configureStore'; export interface Props { dashboardId: number; folder?: FolderInfo; - getDashboardPermissions: typeof getDashboardPermissions; permissions: DashboardAcl[]; + getDashboardPermissions: typeof getDashboardPermissions; + updateDashboardPermission: typeof updateDashboardPermission; + removeDashboardPermission: typeof removeDashboardPermission; + addDashboardPermission: typeof addDashboardPermission; } export interface State { @@ -39,15 +47,15 @@ export class DashboardPermissions extends PureComponent { }; onRemoveItem = (item: DashboardAcl) => { - // this.props.removeFolderPermission(item); + this.props.removeDashboardPermission(this.props.dashboardId, item); }; onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { - // this.props.updateFolderPermission(item, level); + this.props.updateDashboardPermission(this.props.dashboardId, item, level); }; onAddPermission = (newItem: NewDashboardAclItem) => { - // return this.props.addFolderPermission(newItem); + return this.props.addDashboardPermission(this.props.dashboardId, newItem); }; onCancelAddPermission = () => { @@ -101,6 +109,9 @@ const mapStateToProps = (state: StoreState) => ({ const mapDispatchToProps = { getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, }; export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index b1d25d1f57f..82333817b2b 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -68,7 +68,7 @@ export function updateDashboardPermission( itemsToUpdate.push(updated); } - await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); await dispatch(getDashboardPermissions(dashboardId)); }; } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index c86137a55ce..176e270038b 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -17,7 +17,7 @@ import { import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; export interface Props { navModel: NavModel; diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index ff37f13f97f..be45c643e77 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types'; import { inititalState, folderReducer } from './reducers'; function getTestFolder(): FolderDTO { @@ -14,29 +14,85 @@ function getTestFolder(): FolderDTO { } describe('folder reducer', () => { - it('should load folder and set hasChanged to false', () => { - const folder = getTestFolder(); + describe('loadFolder', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); - const action: Action = { - type: ActionTypes.LoadFolder, - payload: folder, - }; + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(false); - expect(state.title).toEqual('test folder'); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); }); - it('should set title', () => { - const action: Action = { - type: ActionTypes.SetFolderTitle, - payload: 'new title', - }; + describe('detFolderTitle', () => { + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(true); - expect(state.title).toEqual('new title'); + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); + }); + + describe('loadFolderPermissions', () => { + let state: FolderState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadFolderPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + { + id: 4, + dashboardId: 10, + permission: PermissionLevel.View, + teamId: 1, + team: 'MyTestTeam', + inherited: true, + }, + { + id: 5, + dashboardId: 1, + permission: PermissionLevel.View, + userId: 1, + userLogin: 'MyTestUser', + }, + { + id: 6, + dashboardId: 1, + permission: PermissionLevel.Edit, + teamId: 2, + team: 'MyTestTeam2', + }, + ], + }; + + state = folderReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(5); + expect(state.permissions.length).toBe(5); + }); + + it('should be sorted by sort rank and alphabetically', async () => { + expect(state.permissions[0].name).toBe('MyTestTeam'); + expect(state.permissions[0].dashboardId).toBe(10); + expect(state.permissions[1].name).toBe('Editor'); + expect(state.permissions[2].name).toBe('Viewer'); + expect(state.permissions[3].name).toBe('MyTestTeam2'); + expect(state.permissions[4].name).toBe('MyTestUser'); + }); }); }); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index feca062b355..d6589f8bf40 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -1,3 +1,9 @@ +export enum OrgRole { + Viewer = 'Viewer', + Editor = 'Editor', + Admin = 'Admin', +} + export interface DashboardAclDTO { id?: number; dashboardId?: number; @@ -7,8 +13,7 @@ export interface DashboardAclDTO { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; inherited?: boolean; } @@ -16,7 +21,7 @@ export interface DashboardAclDTO { export interface DashboardAclUpdateDTO { userId: number; teamId: number; - role: string; + role: OrgRole; permission: PermissionLevel; } @@ -29,8 +34,7 @@ export interface DashboardAcl { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; name?: string; inherited?: boolean; @@ -46,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: string; + role: OrgRole; permission: PermissionLevel; type: AclTarget; } @@ -58,10 +62,10 @@ export enum PermissionLevel { } export enum AclTarget { - Team = 'team', - User = 'user', - Viewer = 'viewer', - Editor = 'editor', + Team = 'Team', + User = 'User', + Viewer = 'Viewer', + Editor = 'Editor', } export interface AclTargetInfo { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index f2fe165a863..8fcfcc7e88d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -4,6 +4,7 @@ import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './loc import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; import { DashboardState } from './dashboard'; +import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; export { Team, @@ -24,6 +25,10 @@ export { FolderDTO, FolderState, FolderInfo, + DashboardState, + DashboardAcl, + OrgRole, + PermissionLevel, }; export interface StoreState { diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index eea3ebbed2d..d367016c4fb 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -24,6 +24,9 @@ module.exports = { path.resolve('node_modules') ], }, + stats: { + warningsFilter: /export .* was not found in/ + }, node: { fs: 'empty', }, From 776d81189f2db5e49577d47b853b1a3b46ca0a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:52:17 +0200 Subject: [PATCH 26/28] test: added simple dashboard reducer test --- public/app/features/folders/state/reducers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index be45c643e77..72e97f39562 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -83,7 +83,6 @@ describe('folder reducer', () => { it('should add permissions to state', async () => { expect(state.permissions.length).toBe(5); - expect(state.permissions.length).toBe(5); }); it('should be sorted by sort rank and alphabetically', async () => { From 331be7d47a9c2f252336c189925a35e8cb2a05d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 08:25:35 +0200 Subject: [PATCH 27/28] fix: add permission fixes --- .../PermissionList/AddPermission.tsx | 26 ++++++++++++------- .../features/dashboard/state/reducers.test.ts | 24 +++++++++++++++++ public/app/types/acl.ts | 2 +- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard/state/reducers.test.ts diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 73bffdaf97b..77ac6953b74 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -26,28 +26,34 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; } onTypeChanged = evt => { - this.setState({ type: evt.target.value as AclTarget }); + const type = evt.target.value as AclTarget; + + switch (type) { + case AclTarget.User: + case AclTarget.Team: + this.setState({ type: type, userId: 0, teamId: 0, role: undefined }); + break; + case AclTarget.Editor: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Editor }); + break; + case AclTarget.Viewer: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Viewer }); + break; + } }; onUserSelected = (user: User) => { - this.setState({ - userId: user ? user.id : 0, - teamId: 0, - }); + this.setState({ userId: user ? user.id : 0 }); }; onTeamSelected = (team: Team) => { - this.setState({ - userId: 0, - teamId: team ? team.id : 0, - }); + this.setState({ teamId: team ? team.id : 0 }); }; onPermissionChanged = (permission: OptionWithDescription) => { diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts new file mode 100644 index 00000000000..c5b67f58ac9 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -0,0 +1,24 @@ +import { Action, ActionTypes } from './actions'; +import { OrgRole, PermissionLevel, DashboardState } from 'app/types'; +import { inititalState, dashboardReducer } from './reducers'; + +describe('dashboard reducer', () => { + describe('loadDashboardPermissions', () => { + let state: DashboardState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadDashboardPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + ], + }; + state = dashboardReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(2); + }); + }); +}); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d6589f8bf40..fa5ace388c4 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -50,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: OrgRole; + role?: OrgRole; permission: PermissionLevel; type: AclTarget; } From 3f309ff5dd2430620bc5ce7bd0a5f731a1466fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 15 Sep 2018 04:50:22 -0700 Subject: [PATCH 28/28] rename folder --- public/app/features/dashboard/all.ts | 2 +- .../DashboardPermissions.tsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename public/app/features/dashboard/{DashboardPermissions => permissions}/DashboardPermissions.tsx (100%) diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 817ed83aaa0..f75743513f1 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,7 +32,7 @@ import './dashlinks/module'; // angular wrappers import { react2AngularDirective } from 'app/core/utils/react2angular'; -import DashboardPermissions from './DashboardPermissions/DashboardPermissions'; +import DashboardPermissions from './permissions/DashboardPermissions'; react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']); diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx similarity index 100% rename from public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx rename to public/app/features/dashboard/permissions/DashboardPermissions.tsx