From 21cfc11009e934dc1a3b7ab33b9859edff48a7af Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 14:34:58 +0200 Subject: [PATCH 01/13] implemented general actionbar --- .../components/OrgActionBar/OrgActionBar.tsx | 38 ++++++++++++ .../datasources/DataSourcesActionBar.test.tsx | 23 ------- .../datasources/DataSourcesActionBar.tsx | 62 ------------------- .../datasources/DataSourcesListPage.tsx | 45 +++++++++++--- .../features/plugins/PluginActionBar.test.tsx | 31 ---------- .../app/features/plugins/PluginActionBar.tsx | 62 ------------------- .../app/features/plugins/PluginListPage.tsx | 30 ++++++--- public/app/features/plugins/state/actions.ts | 2 +- 8 files changed, 100 insertions(+), 193 deletions(-) create mode 100644 public/app/core/components/OrgActionBar/OrgActionBar.tsx delete mode 100644 public/app/features/datasources/DataSourcesActionBar.test.tsx delete mode 100644 public/app/features/datasources/DataSourcesActionBar.tsx delete mode 100644 public/app/features/plugins/PluginActionBar.test.tsx delete mode 100644 public/app/features/plugins/PluginActionBar.tsx diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx new file mode 100644 index 00000000000..fb02985d897 --- /dev/null +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -0,0 +1,38 @@ +import React, { PureComponent } from 'react'; +import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; + +export interface Props { + searchQuery: string; + layoutMode: LayoutMode; + setLayoutMode: (mode: LayoutMode) => {}; + setSearchQuery: (value: string) => {}; + linkButton: { href: string; title: string }; +} + +export default class OrgActionBar extends PureComponent { + render() { + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; + + return ( +
+
+ + setLayoutMode(mode)} /> +
+
+ + {linkButton.title} + +
+ ); + } +} diff --git a/public/app/features/datasources/DataSourcesActionBar.test.tsx b/public/app/features/datasources/DataSourcesActionBar.test.tsx deleted file mode 100644 index 8337271271e..00000000000 --- a/public/app/features/datasources/DataSourcesActionBar.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DataSourcesActionBar, Props } from './DataSourcesActionBar'; -import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; - -const setup = (propOverrides?: object) => { - const props: Props = { - layoutMode: LayoutModes.Grid, - searchQuery: '', - setDataSourcesLayoutMode: jest.fn(), - setDataSourcesSearchQuery: jest.fn(), - }; - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/datasources/DataSourcesActionBar.tsx b/public/app/features/datasources/DataSourcesActionBar.tsx deleted file mode 100644 index d28089b1f21..00000000000 --- a/public/app/features/datasources/DataSourcesActionBar.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; -import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors'; - -export interface Props { - searchQuery: string; - layoutMode: LayoutMode; - setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; - setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; -} - -export class DataSourcesActionBar extends PureComponent { - onSearchQueryChange = event => { - this.props.setDataSourcesSearchQuery(event.target.value); - }; - - render() { - const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props; - - return ( -
-
- - setDataSourcesLayoutMode(mode)} - /> -
- - ); - } -} - -function mapStateToProps(state) { - return { - searchQuery: getDataSourcesSearchQuery(state.dataSources), - layoutMode: getDataSourcesLayoutMode(state.dataSources), - }; -} - -const mapDispatchToProps = { - setDataSourcesLayoutMode, - setDataSourcesSearchQuery, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar); diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index c6db6ee7889..2d18d67a5d2 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -2,21 +2,29 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import PageHeader from '../../core/components/PageHeader/PageHeader'; -import DataSourcesActionBar from './DataSourcesActionBar'; +import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar'; +import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; import DataSourcesList from './DataSourcesList'; -import { loadDataSources } from './state/actions'; -import { getDataSources, getDataSourcesCount, getDataSourcesLayoutMode } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; import { DataSource, NavModel } from 'app/types'; import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; +import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { + getDataSources, + getDataSourcesCount, + getDataSourcesLayoutMode, + getDataSourcesSearchQuery, +} from './state/selectors'; export interface Props { navModel: NavModel; dataSources: DataSource[]; dataSourcesCount: number; layoutMode: LayoutMode; + searchQuery: string; loadDataSources: typeof loadDataSources; + setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; + setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; } const emptyListModel = { @@ -40,7 +48,20 @@ export class DataSourcesListPage extends PureComponent { } render() { - const { dataSources, dataSourcesCount, navModel, layoutMode } = this.props; + const { + dataSources, + dataSourcesCount, + navModel, + layoutMode, + searchQuery, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, + } = this.props; + + const linkButton = { + href: 'datasources/new', + title: 'Add data source', + }; return (
@@ -50,7 +71,14 @@ export class DataSourcesListPage extends PureComponent { ) : ( [ - , + setDataSourcesLayoutMode(mode)} + setSearchQuery={query => setDataSourcesSearchQuery(query)} + linkButton={linkButton} + key="action-bar" + />, , ] )} @@ -66,11 +94,14 @@ function mapStateToProps(state) { dataSources: getDataSources(state.dataSources), layoutMode: getDataSourcesLayoutMode(state.dataSources), dataSourcesCount: getDataSourcesCount(state.dataSources), + searchQuery: getDataSourcesSearchQuery(state.dataSources), }; } const mapDispatchToProps = { loadDataSources, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage)); diff --git a/public/app/features/plugins/PluginActionBar.test.tsx b/public/app/features/plugins/PluginActionBar.test.tsx deleted file mode 100644 index be3f37e89fa..00000000000 --- a/public/app/features/plugins/PluginActionBar.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { PluginActionBar, Props } from './PluginActionBar'; -import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; - -const setup = (propOverrides?: object) => { - const props: Props = { - searchQuery: '', - layoutMode: LayoutModes.Grid, - setLayoutMode: jest.fn(), - setPluginsSearchQuery: jest.fn(), - }; - - Object.assign(props, propOverrides); - - const wrapper = shallow(); - const instance = wrapper.instance() as PluginActionBar; - - return { - wrapper, - instance, - }; -}; - -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/plugins/PluginActionBar.tsx b/public/app/features/plugins/PluginActionBar.tsx deleted file mode 100644 index 301b432ff5c..00000000000 --- a/public/app/features/plugins/PluginActionBar.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import { setLayoutMode, setPluginsSearchQuery } from './state/actions'; -import { getPluginsSearchQuery, getLayoutMode } from './state/selectors'; - -export interface Props { - searchQuery: string; - layoutMode: LayoutMode; - setLayoutMode: typeof setLayoutMode; - setPluginsSearchQuery: typeof setPluginsSearchQuery; -} - -export class PluginActionBar extends PureComponent { - onSearchQueryChange = event => { - this.props.setPluginsSearchQuery(event.target.value); - }; - - render() { - const { searchQuery, layoutMode, setLayoutMode } = this.props; - - return ( -
-
- - setLayoutMode(mode)} /> -
- - ); - } -} - -function mapStateToProps(state) { - return { - searchQuery: getPluginsSearchQuery(state.plugins), - layoutMode: getLayoutMode(state.plugins), - }; -} - -const mapDispatchToProps = { - setPluginsSearchQuery, - setLayoutMode, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index de2968b126c..c549f90ebdd 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -1,20 +1,23 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import PageHeader from '../../core/components/PageHeader/PageHeader'; -import PluginActionBar from './PluginActionBar'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import PluginList from './PluginList'; -import { NavModel, Plugin } from '../../types'; -import { loadPlugins } from './state/actions'; +import { NavModel, Plugin } from 'app/types'; +import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; -import { getLayoutMode, getPlugins } from './state/selectors'; +import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors'; import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; export interface Props { navModel: NavModel; plugins: Plugin[]; layoutMode: LayoutMode; + searchQuery: string; loadPlugins: typeof loadPlugins; + setPluginsLayoutMoode: typeof setPluginsLayoutMode; + setPluginsSearchQuery: typeof setPluginsSearchQuery; } export class PluginListPage extends PureComponent { @@ -27,13 +30,23 @@ export class PluginListPage extends PureComponent { } render() { - const { navModel, plugins, layoutMode } = this.props; + const { navModel, plugins, layoutMode, setPluginsLayoutMoode, setPluginsSearchQuery, searchQuery } = this.props; + const linkButton = { + href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', + title: 'Find more plugins on Grafana.com', + }; return (
- + setPluginsLayoutMoode(mode)} + setSearchQuery={query => setPluginsSearchQuery(query)} + linkButton={linkButton} + /> {plugins && }
@@ -46,11 +59,14 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'plugins'), plugins: getPlugins(state.plugins), layoutMode: getLayoutMode(state.plugins), + searchQuery: getPluginsSearchQuery(state.plugins), }; } const mapDispatchToProps = { loadPlugins, + setPluginsLayoutMode, + setPluginsSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage)); diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts index 24774c6061c..dcfd510ffa0 100644 --- a/public/app/features/plugins/state/actions.ts +++ b/public/app/features/plugins/state/actions.ts @@ -24,7 +24,7 @@ export interface SetLayoutModeAction { payload: LayoutMode; } -export const setLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({ +export const setPluginsLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({ type: ActionTypes.SetLayoutMode, payload: mode, }); From da856187d83e99891bcc9b43e92c5701c670fe3c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 14:57:56 +0200 Subject: [PATCH 02/13] snaps --- .../DataSourcesActionBar.test.tsx.snap | 42 ------------------- .../DataSourcesListPage.test.tsx.snap | 11 ++++- .../features/plugins/PluginListPage.test.tsx | 3 ++ .../PluginActionBar.test.tsx.snap | 40 ------------------ .../PluginListPage.test.tsx.snap | 13 +++++- 5 files changed, 25 insertions(+), 84 deletions(-) delete mode 100644 public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap delete mode 100644 public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap diff --git a/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap deleted file mode 100644 index 24f9f2126d0..00000000000 --- a/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
-
- - -
- -`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index c19ee641e1b..b50600bbc6d 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -8,8 +8,17 @@ exports[`Render should render action bar and datasources 1`] = `
- { const props: Props = { navModel: {} as NavModel, plugins: [] as Plugin[], + searchQuery: '', + setPluginsSearchQuery: jest.fn(), + setPluginsLayoutMoode: jest.fn(), layoutMode: LayoutModes.Grid, loadPlugins: jest.fn(), }; diff --git a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap deleted file mode 100644 index 30cb53cea27..00000000000 --- a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
-
- - -
- -`; diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap index 74b23d8850a..7e837d1ec7d 100644 --- a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -8,7 +8,18 @@ exports[`Render should render component 1`] = `
- + Date: Fri, 28 Sep 2018 17:21:00 +0200 Subject: [PATCH 03/13] first crude display --- .../components/OrgActionBar/OrgActionBar.tsx | 11 +-- .../features/plugins/PluginListPage.test.tsx | 2 +- .../app/features/plugins/PluginListPage.tsx | 7 +- public/app/features/users/UsersListPage.tsx | 66 ++++++++++++++++++ public/app/features/users/UsersTable.tsx | 67 +++++++++++++++++++ public/app/features/users/state/actions.ts | 40 +++++++++++ public/app/features/users/state/reducers.ts | 20 ++++++ public/app/features/users/state/selectors.ts | 2 + public/app/routes/routes.ts | 8 ++- public/app/store/configureStore.ts | 2 + public/app/types/index.ts | 5 ++ public/app/types/users.ts | 15 +++++ 12 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 public/app/features/users/UsersListPage.tsx create mode 100644 public/app/features/users/UsersTable.tsx create mode 100644 public/app/features/users/state/actions.ts create mode 100644 public/app/features/users/state/reducers.ts create mode 100644 public/app/features/users/state/selectors.ts create mode 100644 public/app/types/users.ts diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index fb02985d897..52d74569639 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -3,15 +3,16 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; export interface Props { searchQuery: string; - layoutMode: LayoutMode; - setLayoutMode: (mode: LayoutMode) => {}; + layoutMode?: LayoutMode; + showLayoutMode: boolean; + setLayoutMode?: (mode: LayoutMode) => {}; setSearchQuery: (value: string) => {}; linkButton: { href: string; title: string }; } export default class OrgActionBar extends PureComponent { render() { - const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; return (
@@ -26,7 +27,9 @@ export default class OrgActionBar extends PureComponent { /> - setLayoutMode(mode)} /> + {showLayoutMode && ( + setLayoutMode(mode)} /> + )}
diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx index 699c7d92b1e..b173ef51a2a 100644 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -10,7 +10,7 @@ const setup = (propOverrides?: object) => { plugins: [] as Plugin[], searchQuery: '', setPluginsSearchQuery: jest.fn(), - setPluginsLayoutMoode: jest.fn(), + setPluginsLayoutMode: jest.fn(), layoutMode: LayoutModes.Grid, loadPlugins: jest.fn(), }; diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index c549f90ebdd..22ff0be367f 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -16,7 +16,7 @@ export interface Props { layoutMode: LayoutMode; searchQuery: string; loadPlugins: typeof loadPlugins; - setPluginsLayoutMoode: typeof setPluginsLayoutMode; + setPluginsLayoutMode: typeof setPluginsLayoutMode; setPluginsSearchQuery: typeof setPluginsSearchQuery; } @@ -30,7 +30,7 @@ export class PluginListPage extends PureComponent { } render() { - const { navModel, plugins, layoutMode, setPluginsLayoutMoode, setPluginsSearchQuery, searchQuery } = this.props; + const { navModel, plugins, layoutMode, setPluginsLayoutMode, setPluginsSearchQuery, searchQuery } = this.props; const linkButton = { href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', @@ -42,8 +42,9 @@ export class PluginListPage extends PureComponent {
setPluginsLayoutMoode(mode)} + setLayoutMode={mode => setPluginsLayoutMode(mode)} setSearchQuery={query => setPluginsSearchQuery(query)} linkButton={linkButton} /> diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx new file mode 100644 index 00000000000..4b935845259 --- /dev/null +++ b/public/app/features/users/UsersListPage.tsx @@ -0,0 +1,66 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import UsersTable from 'app/features/users/UsersTable'; +import { NavModel, User } from 'app/types'; +import { loadUsers, setUsersSearchQuery } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getUsers, getUsersSearchQuery } from './state/selectors'; + +export interface Props { + navModel: NavModel; + users: User[]; + searchQuery: string; + loadUsers: typeof loadUsers; + setUsersSearchQuery: typeof setUsersSearchQuery; +} + +export class UsersListPage extends PureComponent { + componentDidMount() { + this.fetchUsers(); + } + + async fetchUsers() { + return await this.props.loadUsers(); + } + render() { + const { navModel, searchQuery, setUsersSearchQuery, users } = this.props; + + const linkButton = { + href: '/org/users/add', + title: 'Add user', + }; + + return ( +
+ +
+ + +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'users'), + users: getUsers(state.users), + searchQuery: getUsersSearchQuery(state.users), + }; +} + +const mapDispatchToProps = { + loadUsers, + setUsersSearchQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage)); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx new file mode 100644 index 00000000000..38ff720472f --- /dev/null +++ b/public/app/features/users/UsersTable.tsx @@ -0,0 +1,67 @@ +import React, { SFC } from 'react'; +import { User } from 'app/types'; + +export interface Props { + users: User[]; + onRoleChange: (value: string) => {}; +} + +const UsersTable: SFC = props => { + const { users } = props; + + return ( +
+ Le Table + + + + + + + + + + {users.map((user, index) => { + return ( + + + + + + + + + ); + })} +
+ LoginEmailSeenRole +
+ + {user.login} + {user.email} + {user.lastSeenAtAge} +
+ +
+
+
props.removeUser(user)} className="btn btn-danger btn-mini"> + +
+
+
+ ); +}; + +export default UsersTable; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts new file mode 100644 index 00000000000..0bda6b0c58a --- /dev/null +++ b/public/app/features/users/state/actions.ts @@ -0,0 +1,40 @@ +import { ThunkAction } from 'redux-thunk'; +import { StoreState } from '../../../types'; +import { getBackendSrv } from '../../../core/services/backend_srv'; +import { User } from 'app/types'; + +export enum ActionTypes { + LoadUsers = 'LOAD_USERS', + SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY', +} + +export interface LoadUsersAction { + type: ActionTypes.LoadUsers; + payload: User[]; +} + +export interface SetUsersSearchQueryAction { + type: ActionTypes.SetUsersSearchQuery; + payload: string; +} + +const usersLoaded = (users: User[]): LoadUsersAction => ({ + type: ActionTypes.LoadUsers, + payload: users, +}); + +export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({ + type: ActionTypes.SetUsersSearchQuery, + payload: query, +}); + +export type Action = LoadUsersAction | SetUsersSearchQueryAction; + +type ThunkResult = ThunkAction; + +export function loadUsers(): ThunkResult { + return async dispatch => { + const users = await getBackendSrv().get('/api/org/users'); + dispatch(usersLoaded(users)); + }; +} diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts new file mode 100644 index 00000000000..1bf62ba9d2e --- /dev/null +++ b/public/app/features/users/state/reducers.ts @@ -0,0 +1,20 @@ +import { User, UsersState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialState: UsersState = { users: [] as User[], searchQuery: '' }; + +export const usersReducer = (state = initialState, action: Action): UsersState => { + switch (action.type) { + case ActionTypes.LoadUsers: + return { ...state, users: action.payload }; + + case ActionTypes.SetUsersSearchQuery: + return { ...state, searchQuery: action.payload }; + } + + return state; +}; + +export default { + users: usersReducer, +}; diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts new file mode 100644 index 00000000000..8882c5e56e4 --- /dev/null +++ b/public/app/features/users/state/selectors.ts @@ -0,0 +1,2 @@ +export const getUsers = state => state.users; +export const getUsersSearchQuery = state => state.searchQuery; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 8f17dce9757..8a83db3e1cd 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -9,6 +9,7 @@ import PluginListPage from 'app/features/plugins/PluginListPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; +import UsersListPage from 'app/features/users/UsersListPage'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -131,9 +132,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'NewOrgCtrl', }) .when('/org/users', { - templateUrl: 'public/app/features/org/partials/orgUsers.html', - controller: 'OrgUsersCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => UsersListPage, + }, }) .when('/org/users/invite', { templateUrl: 'public/app/features/org/partials/invite.html', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 6313bddfb3a..0ca22f6988a 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -8,6 +8,7 @@ import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; +import usersReducers from 'app/features/users/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, @@ -17,6 +18,7 @@ const rootReducer = combineReducers({ ...dashboardReducers, ...pluginReducers, ...dataSourcesReducers, + ...usersReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 3dbef72ce17..f2518c2bc75 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -7,6 +7,7 @@ import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; +import { User, UsersState } from './users'; export { Team, @@ -36,6 +37,8 @@ export { Plugin, PluginsState, DataSourcesState, + User, + UsersState, }; export interface StoreState { @@ -46,4 +49,6 @@ export interface StoreState { team: TeamState; folder: FolderState; dashboard: DashboardState; + dataSources: DataSourcesState; + users: UsersState; } diff --git a/public/app/types/users.ts b/public/app/types/users.ts new file mode 100644 index 00000000000..74e7195d868 --- /dev/null +++ b/public/app/types/users.ts @@ -0,0 +1,15 @@ +export interface User { + avatarUrl: string; + email: string; + lastSeenAt: string; + lastSeenAtAge: string; + login: string; + orgId: number; + role: string; + userId: number; +} + +export interface UsersState { + users: User[]; + searchQuery: string; +} From 94971abd9c789fb1acbc961e972b7d5483a31f3a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 12:01:53 +0200 Subject: [PATCH 04/13] functions and tests --- .../app/features/users/UsersListPage.test.tsx | 51 ++ public/app/features/users/UsersListPage.tsx | 32 +- public/app/features/users/UsersTable.test.tsx | 33 ++ public/app/features/users/UsersTable.tsx | 80 ++-- .../app/features/users/__mocks__/userMocks.ts | 31 ++ .../__snapshots__/UsersListPage.test.tsx.snap | 29 ++ .../__snapshots__/UsersTable.test.tsx.snap | 448 ++++++++++++++++++ public/app/features/users/state/actions.ts | 14 + 8 files changed, 677 insertions(+), 41 deletions(-) create mode 100644 public/app/features/users/UsersListPage.test.tsx create mode 100644 public/app/features/users/UsersTable.test.tsx create mode 100644 public/app/features/users/__mocks__/userMocks.ts create mode 100644 public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap create mode 100644 public/app/features/users/__snapshots__/UsersTable.test.tsx.snap diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx new file mode 100644 index 00000000000..8ba8ec4b06c --- /dev/null +++ b/public/app/features/users/UsersListPage.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { UsersListPage, Props } from './UsersListPage'; +import { NavModel, User } from 'app/types'; +import { getMockUser } from './__mocks__/userMocks'; +import appEvents from '../../core/app_events'; + +jest.mock('../../core/app_events', () => ({ + emit: jest.fn(), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + users: [] as User[], + searchQuery: '', + loadUsers: jest.fn(), + updateUser: jest.fn(), + removeUser: jest.fn(), + setUsersSearchQuery: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as UsersListPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should emit show remove user modal', () => { + const { instance } = setup(); + const mockUser = getMockUser(); + + instance.onRemoveUser(mockUser); + + expect(appEvents.emit).toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 4b935845259..88c290e80ff 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -5,7 +5,8 @@ import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import UsersTable from 'app/features/users/UsersTable'; import { NavModel, User } from 'app/types'; -import { loadUsers, setUsersSearchQuery } from './state/actions'; +import appEvents from 'app/core/app_events'; +import { loadUsers, setUsersSearchQuery, updateUser, removeUser } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; import { getUsers, getUsersSearchQuery } from './state/selectors'; @@ -15,6 +16,8 @@ export interface Props { searchQuery: string; loadUsers: typeof loadUsers; setUsersSearchQuery: typeof setUsersSearchQuery; + updateUser: typeof updateUser; + removeUser: typeof removeUser; } export class UsersListPage extends PureComponent { @@ -25,6 +28,25 @@ export class UsersListPage extends PureComponent { async fetchUsers() { return await this.props.loadUsers(); } + + onRoleChange = (role, user) => { + const updatedUser = { ...user, role: role }; + + this.props.updateUser(updatedUser); + }; + + onRemoveUser = user => { + appEvents.emit('confirm-modal', { + title: 'Delete', + text: 'Are you sure you want to delete user ' + user.login + '?', + yesText: 'Delete', + icon: 'fa-warning', + onConfirm: () => { + this.props.removeUser(user.userId); + }, + }); + }; + render() { const { navModel, searchQuery, setUsersSearchQuery, users } = this.props; @@ -43,7 +65,11 @@ export class UsersListPage extends PureComponent { setSearchQuery={setUsersSearchQuery} linkButton={linkButton} /> - + this.onRoleChange(role, user)} + onRemoveUser={user => this.onRemoveUser(user)} + />
); @@ -61,6 +87,8 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadUsers, setUsersSearchQuery, + updateUser, + removeUser, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage)); diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx new file mode 100644 index 00000000000..8cbfb0b4e6f --- /dev/null +++ b/public/app/features/users/UsersTable.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import UsersTable, { Props } from './UsersTable'; +import { User } from 'app/types'; +import { getMockUsers } from './__mocks__/userMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + users: [] as User[], + onRoleChange: jest.fn(), + onRemoveUser: jest.fn(), + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render users table', () => { + const wrapper = setup({ + users: getMockUsers(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 38ff720472f..e9b5edd7acb 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -3,15 +3,15 @@ import { User } from 'app/types'; export interface Props { users: User[]; - onRoleChange: (value: string) => {}; + onRoleChange: (role: string, user: User) => void; + onRemoveUser: (user: User) => void; } const UsersTable: SFC = props => { - const { users } = props; + const { users, onRoleChange, onRemoveUser } = props; return (
- Le Table @@ -23,42 +23,44 @@ const UsersTable: SFC = props => { - {users.map((user, index) => { - return ( - - - - - - - - - ); - })} + + {users.map((user, index) => { + return ( + + + + + + + + + ); + })} +
- - {user.login} - {user.email} - {user.lastSeenAtAge} -
- -
-
-
props.removeUser(user)} className="btn btn-danger btn-mini"> - -
-
+ + {user.login} + {user.email} + {user.lastSeenAtAge} +
+ +
+
+
onRemoveUser(user)} className="btn btn-danger btn-mini"> + +
+
); diff --git a/public/app/features/users/__mocks__/userMocks.ts b/public/app/features/users/__mocks__/userMocks.ts new file mode 100644 index 00000000000..ef7789458d0 --- /dev/null +++ b/public/app/features/users/__mocks__/userMocks.ts @@ -0,0 +1,31 @@ +export const getMockUsers = (amount: number) => { + const users = []; + + for (let i = 0; i <= amount; i++) { + users.push({ + avatarUrl: 'url/to/avatar', + email: `user-${i}@test.com`, + lastSeenAt: '2018-10-01', + lastSeenAtAge: '', + login: `user-${i}`, + orgId: 1, + role: 'Admin', + userId: i, + }); + } + + return users; +}; + +export const getMockUser = () => { + return { + avatarUrl: 'url/to/avatar', + email: `user@test.com`, + lastSeenAt: '2018-10-01', + lastSeenAtAge: '', + login: `user`, + orgId: 1, + role: 'Admin', + userId: 2, + }; +}; diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap new file mode 100644 index 00000000000..689e7bd007b --- /dev/null +++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+ + +
+
+`; diff --git a/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap b/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap new file mode 100644 index 00000000000..9dace6a730f --- /dev/null +++ b/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ + + + + + + + + + +
+ + Login + + Email + + Seen + + Role + +
+
+`; + +exports[`Render should render users table 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Login + + Email + + Seen + + Role + +
+ + + user-0 + + + user-0@test.com + + + +
+ +
+
+
+ +
+
+ + + user-1 + + + user-1@test.com + + + +
+ +
+
+
+ +
+
+ + + user-2 + + + user-2@test.com + + + +
+ +
+
+
+ +
+
+ + + user-3 + + + user-3@test.com + + + +
+ +
+
+
+ +
+
+ + + user-4 + + + user-4@test.com + + + +
+ +
+
+
+ +
+
+ + + user-5 + + + user-5@test.com + + + +
+ +
+
+
+ +
+
+
+`; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts index 0bda6b0c58a..2c35fb06e7a 100644 --- a/public/app/features/users/state/actions.ts +++ b/public/app/features/users/state/actions.ts @@ -38,3 +38,17 @@ export function loadUsers(): ThunkResult { dispatch(usersLoaded(users)); }; } + +export function updateUser(user: User): ThunkResult { + return async dispatch => { + await getBackendSrv().patch(`/api/org/users/${user.userId}`, user); + dispatch(loadUsers()); + }; +} + +export function removeUser(userId: number): ThunkResult { + return async dispatch => { + await getBackendSrv().delete(`/api/org/users/${userId}`); + dispatch(loadUsers()); + }; +} From 3211df7303b922d7899ed7a973039e1a3f430570 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 13:45:00 +0200 Subject: [PATCH 05/13] filter users in selector based on search --- public/app/features/users/state/selectors.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts index 8882c5e56e4..4799cba0e27 100644 --- a/public/app/features/users/state/selectors.ts +++ b/public/app/features/users/state/selectors.ts @@ -1,2 +1,9 @@ -export const getUsers = state => state.users; +export const getUsers = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.users.filter(user => { + return regex.test(user.login) || regex.test(user.email); + }); +}; + export const getUsersSearchQuery = state => state.searchQuery; From a43ede70bc9268aed64a9df0240157bf8ccab9d1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 14:02:13 +0200 Subject: [PATCH 06/13] added default prop instead of specifying prop --- public/app/core/components/OrgActionBar/OrgActionBar.tsx | 4 ++++ public/app/features/datasources/DataSourcesListPage.test.tsx | 3 +++ public/app/features/plugins/PluginListPage.tsx | 1 - 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index 52d74569639..f3112ffaefd 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -11,6 +11,10 @@ export interface Props { } export default class OrgActionBar extends PureComponent { + static defaultProps = { + showLayoutMode: true, + }; + render() { const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index fed7954d716..96f6c304b16 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -12,6 +12,9 @@ const setup = (propOverrides?: object) => { loadDataSources: jest.fn(), navModel: {} as NavModel, dataSourcesCount: 0, + searchQuery: '', + setDataSourcesSearchQuery: jest.fn(), + setDataSourcesLayoutMode: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 22ff0be367f..c24d44d6826 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -42,7 +42,6 @@ export class PluginListPage extends PureComponent {
setPluginsLayoutMode(mode)} setSearchQuery={query => setPluginsSearchQuery(query)} From 13666c8462b4f25f11496dc6e834a593050d4eea Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 14:17:28 +0200 Subject: [PATCH 07/13] tests --- .../OrgActionBar/OrgActionBar.test.tsx | 32 ++++++++ .../__snapshots__/OrgActionBar.test.tsx.snap | 74 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 public/app/core/components/OrgActionBar/OrgActionBar.test.tsx create mode 100644 public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx new file mode 100644 index 00000000000..d1edeeaa779 --- /dev/null +++ b/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import OrgActionBar, { Props } from './OrgActionBar'; + +const setup = (propOverrides?: object) => { + const props: Props = { + searchQuery: '', + showLayoutMode: true, + setSearchQuery: jest.fn(), + linkButton: { href: 'some/url', title: 'test' }, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should hide layout mode', () => { + const wrapper = setup({ + showLayoutMode: false, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap new file mode 100644 index 00000000000..9fdae04975d --- /dev/null +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should hide layout mode 1`] = ` +
+
+ +
+
+`; + +exports[`Render should render component 1`] = ` +
+
+ + +
+ +`; From 3c8820ab55bb43b21d4b938f82ad821486cf33ed Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 18:01:26 +0200 Subject: [PATCH 08/13] invites table --- .../components/OrgActionBar/OrgActionBar.tsx | 11 +- public/app/features/users/InviteesTable.tsx | 59 ++++++++++ public/app/features/users/UsersActionBar.tsx | 80 ++++++++++++++ public/app/features/users/UsersListPage.tsx | 75 +++++++++---- public/app/features/users/UsersTable.tsx | 102 +++++++++--------- public/app/features/users/state/actions.ts | 29 ++++- public/app/features/users/state/reducers.ts | 16 ++- public/app/features/users/state/selectors.ts | 9 ++ public/app/types/index.ts | 3 +- public/app/types/users.ts | 22 ++++ 10 files changed, 318 insertions(+), 88 deletions(-) create mode 100644 public/app/features/users/InviteesTable.tsx create mode 100644 public/app/features/users/UsersActionBar.tsx diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index f3112ffaefd..de91c6cc6b3 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -4,19 +4,14 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; export interface Props { searchQuery: string; layoutMode?: LayoutMode; - showLayoutMode: boolean; setLayoutMode?: (mode: LayoutMode) => {}; setSearchQuery: (value: string) => {}; linkButton: { href: string; title: string }; } export default class OrgActionBar extends PureComponent { - static defaultProps = { - showLayoutMode: true, - }; - render() { - const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; return (
@@ -31,9 +26,7 @@ export default class OrgActionBar extends PureComponent { /> - {showLayoutMode && ( - setLayoutMode(mode)} /> - )} + setLayoutMode(mode)} />
diff --git a/public/app/features/users/InviteesTable.tsx b/public/app/features/users/InviteesTable.tsx new file mode 100644 index 00000000000..82c02e607de --- /dev/null +++ b/public/app/features/users/InviteesTable.tsx @@ -0,0 +1,59 @@ +import React, { createRef, PureComponent } from 'react'; +import { Invitee } from 'app/types'; + +export interface Props { + invitees: Invitee[]; + revokeInvite: (code: string) => void; +} + +export default class InviteesTable extends PureComponent { + private copyRef = createRef(); + + copyToClipboard = () => { + const node = this.copyRef.current; + + if (node) { + node.select(); + document.execCommand('copy'); + } + }; + + render() { + const { invitees, revokeInvite } = this.props; + + return ( + + + + + + + + + {invitees.map((invitee, index) => { + return ( + + + +
EmailName + +
{invitee.email}{invitee.name} +