diff --git a/.gitignore b/.gitignore
index 12e7bed3f46..deb1c1f882a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,4 @@ debug.test
/vendor/**/*_test.go
/vendor/**/.editorconfig
/vendor/**/appengine*
+*.orig
\ No newline at end of file
diff --git a/package.json b/package.json
index 2f79acc41df..be753596a50 100644
--- a/package.json
+++ b/package.json
@@ -151,6 +151,7 @@
"react-grid-layout": "^0.16.1",
"react-popper": "^0.7.5",
"react-highlight-words": "^0.10.0",
+ "react-select": "^1.1.0",
"react-sizeme": "^2.3.6",
"remarkable": "^1.7.1",
"rxjs": "^5.4.3",
diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts
index 839ff011d5a..6e68e7c8d2f 100644
--- a/public/app/core/angular_wrappers.ts
+++ b/public/app/core/angular_wrappers.ts
@@ -1,14 +1,16 @@
-import { react2AngularDirective } from "app/core/utils/react2angular";
-import { PasswordStrength } from "./components/PasswordStrength";
-import PageHeader from "./components/PageHeader/PageHeader";
-import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA";
-import LoginBackground from "./components/Login/LoginBackground";
-import { SearchResult } from "./components/search/SearchResult";
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { PasswordStrength } from './components/PasswordStrength';
+import PageHeader from './components/PageHeader/PageHeader';
+import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
+import LoginBackground from './components/Login/LoginBackground';
+import { SearchResult } from './components/search/SearchResult';
+import UserPicker from './components/UserPicker/UserPicker';
export function registerAngularDirectives() {
- react2AngularDirective("passwordStrength", PasswordStrength, ["password"]);
- react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]);
- react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]);
- react2AngularDirective("loginBackground", LoginBackground, []);
- react2AngularDirective("searchResult", SearchResult, []);
+ react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
+ react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
+ react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
+ react2AngularDirective('loginBackground', LoginBackground, []);
+ react2AngularDirective('searchResult', SearchResult, []);
+ react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']);
}
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
index d62ae892a0a..4af60f3c839 100644
--- a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
@@ -3,19 +3,18 @@ import renderer from 'react-test-renderer';
import EmptyListCTA from './EmptyListCTA';
const model = {
- title: 'Title',
- buttonIcon: 'ga css class',
- buttonLink: 'http://url/to/destination',
- buttonTitle: 'Click me',
- proTip: 'This is a tip',
- proTipLink: 'http://url/to/tip/destination',
- proTipLinkTitle: 'Learn more',
- proTipTarget: '_blank'
+ title: 'Title',
+ buttonIcon: 'ga css class',
+ buttonLink: 'http://url/to/destination',
+ buttonTitle: 'Click me',
+ proTip: 'This is a tip',
+ proTipLink: 'http://url/to/tip/destination',
+ proTipLinkTitle: 'Learn more',
+ proTipTarget: '_blank',
};
-describe('CollorPalette', () => {
-
- it('renders correctly', () => {
+describe('EmptyListCTA', () => {
+ it('renders correctly', () => {
const tree = renderer.create().toJSON();
expect(tree).toMatchSnapshot();
});
diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap
index 0da3d94aaa8..6d47c984d5e 100644
--- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap
+++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`CollorPalette renders correctly 1`] = `
+exports[`EmptyListCTA renders correctly 1`] = `
diff --git a/public/app/core/components/UserPicker/UserPicker.jest.tsx b/public/app/core/components/UserPicker/UserPicker.jest.tsx
new file mode 100644
index 00000000000..0e3c672dcbe
--- /dev/null
+++ b/public/app/core/components/UserPicker/UserPicker.jest.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import UserPicker from './UserPicker';
+
+const model = {
+ backendSrv: {
+ get: () => {
+ return new Promise((resolve, reject) => {});
+ },
+ },
+ refreshList: () => {},
+ teamId: '1',
+};
+
+describe('UserPicker', () => {
+ it('renders correctly', () => {
+ const tree = renderer.create(
).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/UserPicker/UserPicker.tsx b/public/app/core/components/UserPicker/UserPicker.tsx
new file mode 100644
index 00000000000..12aca15e510
--- /dev/null
+++ b/public/app/core/components/UserPicker/UserPicker.tsx
@@ -0,0 +1,108 @@
+import React, { Component } from 'react';
+import { debounce } from 'lodash';
+import Select from 'react-select';
+import UserPickerOption from './UserPickerOption';
+
+export interface IProps {
+ backendSrv: any;
+ teamId: string;
+ refreshList: any;
+}
+
+export interface User {
+ id: number;
+ name: string;
+ login: string;
+ email: string;
+}
+
+class UserPicker extends Component
{
+ debouncedSearchUsers: any;
+ backendSrv: any;
+ teamId: string;
+ refreshList: any;
+
+ constructor(props) {
+ super(props);
+ this.backendSrv = this.props.backendSrv;
+ this.teamId = this.props.teamId;
+ this.refreshList = this.props.refreshList;
+
+ this.searchUsers = this.searchUsers.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.addUser = this.addUser.bind(this);
+ this.toggleLoading = this.toggleLoading.bind(this);
+
+ this.debouncedSearchUsers = debounce(this.searchUsers, 300, {
+ leading: true,
+ trailing: false,
+ });
+
+ this.state = {
+ multi: false,
+ isLoading: false,
+ };
+ }
+
+ handleChange(user) {
+ this.addUser(user.id);
+ }
+
+ toggleLoading(isLoading) {
+ this.setState(prevState => {
+ return {
+ ...prevState,
+ isLoading: isLoading,
+ };
+ });
+ }
+
+ addUser(userId) {
+ this.toggleLoading(true);
+ this.backendSrv.post(`/api/teams/${this.teamId}/members`, { userId: userId }).then(() => {
+ this.refreshList();
+ this.toggleLoading(false);
+ });
+ }
+
+ searchUsers(query) {
+ this.toggleLoading(true);
+
+ return this.backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => {
+ const users = result.users.map(user => {
+ return {
+ id: user.id,
+ label: `${user.login} - ${user.email}`,
+ avatarUrl: user.avatarUrl,
+ };
+ });
+ this.toggleLoading(false);
+ return { options: users };
+ });
+ }
+
+ render() {
+ const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
+
+ return (
+
+ );
+ }
+}
+
+export default UserPicker;
diff --git a/public/app/core/components/UserPicker/UserPickerOption.jest.tsx b/public/app/core/components/UserPicker/UserPickerOption.jest.tsx
new file mode 100644
index 00000000000..6903d648870
--- /dev/null
+++ b/public/app/core/components/UserPicker/UserPickerOption.jest.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import UserPickerOption from './UserPickerOption';
+
+const model = {
+ onSelect: () => {},
+ onFocus: () => {},
+ isFocused: () => {},
+ option: {
+ title: 'Model title',
+ avatarUrl: 'url/to/avatar',
+ label: 'User picker label',
+ },
+ className: 'class-for-user-picker',
+};
+
+describe('UserPickerOption', () => {
+ it('renders correctly', () => {
+ const tree = renderer.create().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/UserPicker/UserPickerOption.tsx b/public/app/core/components/UserPicker/UserPickerOption.tsx
new file mode 100644
index 00000000000..42a079802a8
--- /dev/null
+++ b/public/app/core/components/UserPicker/UserPickerOption.tsx
@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+export interface IProps {
+ onSelect: any;
+ onFocus: any;
+ option: any;
+ isFocused: any;
+ className: any;
+}
+class UserPickerOption extends Component {
+ constructor(props) {
+ super(props);
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleMouseEnter = this.handleMouseEnter.bind(this);
+ this.handleMouseMove = this.handleMouseMove.bind(this);
+ }
+
+ handleMouseDown(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.props.onSelect(this.props.option, event);
+ }
+
+ handleMouseEnter(event) {
+ this.props.onFocus(this.props.option, event);
+ }
+
+ handleMouseMove(event) {
+ if (this.props.isFocused) {
+ return;
+ }
+ this.props.onFocus(this.props.option, event);
+ }
+
+ render() {
+ const { option, children, className } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+export default UserPickerOption;
diff --git a/public/app/core/components/UserPicker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/UserPicker/__snapshots__/UserPicker.jest.tsx.snap
new file mode 100644
index 00000000000..26e331b7403
--- /dev/null
+++ b/public/app/core/components/UserPicker/__snapshots__/UserPicker.jest.tsx.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UserPicker renders correctly 1`] = `
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap b/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap
new file mode 100644
index 00000000000..3b9351b2338
--- /dev/null
+++ b/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UserPickerOption renders correctly 1`] = `
+
+`;
diff --git a/public/app/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html
index 4835facb785..e2666697869 100644
--- a/public/app/features/org/partials/team_details.html
+++ b/public/app/features/org/partials/team_details.html
@@ -26,11 +26,10 @@