mirror of https://github.com/grafana/grafana
commit
7854f80f5a
@ -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']); |
||||
} |
||||
|
@ -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,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> |
||||
`; |
@ -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%); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue