mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/10289_user_picker'
This commit is contained in:
commit
7854f80f5a
1
.gitignore
vendored
1
.gitignore
vendored
@ -60,3 +60,4 @@ debug.test
|
||||
/vendor/**/*_test.go
|
||||
/vendor/**/.editorconfig
|
||||
/vendor/**/appengine*
|
||||
*.orig
|
@ -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",
|
||||
|
@ -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']);
|
||||
}
|
||||
|
@ -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(<EmptyListCTA model={model} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollorPalette renders correctly 1`] = `
|
||||
exports[`EmptyListCTA renders correctly 1`] = `
|
||||
<div
|
||||
className="empty-list-cta"
|
||||
>
|
||||
|
20
public/app/core/components/UserPicker/UserPicker.jest.tsx
Normal file
20
public/app/core/components/UserPicker/UserPicker.jest.tsx
Normal file
@ -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(<UserPicker {...model} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
108
public/app/core/components/UserPicker/UserPicker.tsx
Normal file
108
public/app/core/components/UserPicker/UserPicker.tsx
Normal file
@ -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<IProps, any> {
|
||||
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 (
|
||||
<div className="user-picker">
|
||||
<AsyncComponent
|
||||
valueKey="id"
|
||||
multi={this.state.multi}
|
||||
labelKey="label"
|
||||
cache={false}
|
||||
isLoading={this.state.isLoading}
|
||||
loadOptions={this.debouncedSearchUsers}
|
||||
loadingPlaceholder="Loading..."
|
||||
noResultsText="No users found"
|
||||
onChange={this.handleChange}
|
||||
className="width-8 gf-form-input gf-form-input--form-dropdown"
|
||||
optionComponent={UserPickerOption}
|
||||
placeholder="Choose"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserPicker;
|
@ -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(<UserPickerOption {...model} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
52
public/app/core/components/UserPicker/UserPickerOption.tsx
Normal file
52
public/app/core/components/UserPicker/UserPickerOption.tsx
Normal file
@ -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<IProps, any> {
|
||||
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 (
|
||||
<button
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
title={option.title}
|
||||
className={`user-picker-option__button btn btn-link ${className}`}
|
||||
>
|
||||
<img src={option.avatarUrl} alt={option.label} className="user-picker-option__avatar" />
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserPickerOption;
|
@ -0,0 +1,98 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UserPicker renders correctly 1`] = `
|
||||
<div
|
||||
className="user-picker"
|
||||
>
|
||||
<div
|
||||
className="Select width-8 gf-form-input is-clearable is-loading is-searchable Select--single"
|
||||
style={undefined}
|
||||
>
|
||||
<div
|
||||
className="Select-control"
|
||||
onKeyDown={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={undefined}
|
||||
>
|
||||
<span
|
||||
className="Select-multi-value-wrapper"
|
||||
id="react-select-2--value"
|
||||
>
|
||||
<div
|
||||
className="Select-placeholder"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
className="Select-input"
|
||||
style={
|
||||
Object {
|
||||
"display": "inline-block",
|
||||
}
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="react-select-2--value"
|
||||
aria-describedby={undefined}
|
||||
aria-expanded="false"
|
||||
aria-haspopup="false"
|
||||
aria-label={undefined}
|
||||
aria-labelledby={undefined}
|
||||
aria-owns=""
|
||||
className={undefined}
|
||||
id={undefined}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={false}
|
||||
role="combobox"
|
||||
style={
|
||||
Object {
|
||||
"boxSizing": "content-box",
|
||||
"width": "5px",
|
||||
}
|
||||
}
|
||||
tabIndex={undefined}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
"left": 0,
|
||||
"overflow": "scroll",
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"visibility": "hidden",
|
||||
"whiteSpace": "pre",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Select-loading-zone"
|
||||
>
|
||||
<span
|
||||
className="Select-loading"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="Select-arrow-zone"
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
<span
|
||||
className="Select-arrow"
|
||||
onMouseDown={[Function]}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UserPickerOption renders correctly 1`] = `
|
||||
<button
|
||||
className="user-picker-option__button btn btn-link class-for-user-picker"
|
||||
onMouseDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
title="Model title"
|
||||
>
|
||||
<img
|
||||
alt="User picker label"
|
||||
className="user-picker-option__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</button>
|
||||
`;
|
@ -26,11 +26,10 @@
|
||||
<div class="gf-form-group">
|
||||
|
||||
<h3 class="page-heading">Team Members</h3>
|
||||
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Add member</span>
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
<select-user-picker backendSrv="ctrl.backendSrv" teamId="ctrl.$routeParams.id" refreshList="ctrl.get" teamMembers="ctrl.teamMembers"></select-user-picker>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -8,6 +8,7 @@ export default class TeamDetailsCtrl {
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
||||
this.get = this.get.bind(this);
|
||||
this.get();
|
||||
}
|
||||
|
||||
@ -35,7 +36,7 @@ export default class TeamDetailsCtrl {
|
||||
}
|
||||
|
||||
removeMemberConfirmed(teamMember: TeamMember) {
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get.bind(this));
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get);
|
||||
}
|
||||
|
||||
update() {
|
||||
|
@ -89,6 +89,8 @@
|
||||
@import 'components/dashboard_settings';
|
||||
@import 'components/empty_list_cta';
|
||||
@import 'components/popper';
|
||||
@import 'components/form_select_box';
|
||||
@import 'components/user-picker';
|
||||
|
||||
// PAGES
|
||||
@import 'pages/login';
|
||||
|
73
public/sass/components/_form_select_box.scss
Normal file
73
public/sass/components/_form_select_box.scss
Normal file
@ -0,0 +1,73 @@
|
||||
$select-input-height: 35px;
|
||||
$select-menu-max-height: 300px;
|
||||
$select-item-font-size: $font-size-base;
|
||||
$select-item-bg: $dropdownBackground;
|
||||
$select-item-fg: $input-color;
|
||||
$select-option-bg: $dropdownBackground;
|
||||
$select-option-color: $input-color;
|
||||
$select-noresults-color: $text-color;
|
||||
$select-input-bg: $input-bg;
|
||||
$select-input-border-color: $input-border-color;
|
||||
$select-menu-box-shadow: $menu-dropdown-shadow;
|
||||
|
||||
@import '../../../node_modules/react-select/scss/default.scss';
|
||||
|
||||
@mixin select-control() {
|
||||
width: 100%;
|
||||
margin-right: $gf-form-margin;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
@mixin select-control-focus() {
|
||||
border-color: $input-border-focus;
|
||||
outline: none;
|
||||
$shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px $input-box-shadow-focus;
|
||||
@include box-shadow($shadow);
|
||||
}
|
||||
|
||||
.gf-form-input--form-dropdown {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
|
||||
.Select-placeholder {
|
||||
color: $gray-4;
|
||||
}
|
||||
|
||||
> .Select-control {
|
||||
@include select-control();
|
||||
border-color: $dark-3;
|
||||
}
|
||||
|
||||
&.is-open > .Select-control {
|
||||
background: transparent;
|
||||
border-color: $dark-3;
|
||||
}
|
||||
|
||||
&.is-focused > .Select-control {
|
||||
background-color: $input-bg;
|
||||
@include select-control-focus();
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
border: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Select-option.is-focused {
|
||||
background-color: $dropdownLinkBackgroundHover;
|
||||
color: $dropdownLinkColorHover;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
display: block;
|
||||
content: '';
|
||||
background-image: linear-gradient(to bottom, #ffd500 0%, #ff4400 99%, #ff4400 100%);
|
||||
}
|
||||
}
|
||||
}
|
12
public/sass/components/_user-picker.scss
Normal file
12
public/sass/components/_user-picker.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.user-picker-option__button {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 0;
|
||||
}
|
||||
.user-picker-option__avatar {
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
30
yarn.lock
30
yarn.lock
@ -1706,7 +1706,7 @@ class-utils@^0.3.5:
|
||||
lazy-cache "^2.0.2"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@2.x, classnames@^2.2.5:
|
||||
classnames@2.x, classnames@^2.2.4, classnames@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
|
||||
|
||||
@ -8131,13 +8131,6 @@ react-grid-layout@^0.16.1:
|
||||
react-draggable "^3.0.3"
|
||||
react-resizable "^1.7.5"
|
||||
|
||||
react-popper@^0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596"
|
||||
dependencies:
|
||||
popper.js "^1.12.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-highlight-words@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.10.0.tgz#2e905c76c11635237f848ecad00600f1b6f6f4a8"
|
||||
@ -8145,6 +8138,19 @@ react-highlight-words@^0.10.0:
|
||||
highlight-words-core "^1.1.0"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-input-autosize@^2.1.2:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
|
||||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-popper@^0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596"
|
||||
dependencies:
|
||||
popper.js "^1.12.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-resizable@^1.7.5:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
|
||||
@ -8152,6 +8158,14 @@ react-resizable@^1.7.5:
|
||||
prop-types "15.x"
|
||||
react-draggable "^2.2.6 || ^3.0.3"
|
||||
|
||||
react-select@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.0.tgz#4f91df941c4ecdb94701faca2533b60e31d7508e"
|
||||
dependencies:
|
||||
classnames "^2.2.4"
|
||||
prop-types "^15.5.8"
|
||||
react-input-autosize "^2.1.2"
|
||||
|
||||
react-sizeme@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678"
|
||||
|
Loading…
Reference in New Issue
Block a user