From 5931d6c87d7f42f34bf25bffc96eb1e1cd1ce9fb Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 20 Dec 2017 16:52:43 +0100 Subject: [PATCH] ux: POC on new select box for the user picker (#10289) --- package.json | 1 + public/app/core/angular_wrappers.ts | 6 + .../core/components/UserPicker/UserPicker.tsx | 118 +++++++++++++ .../UserPicker/UserPickerOption.tsx | 48 +++++ .../features/org/partials/team_details.html | 9 +- public/app/features/org/team_details_ctrl.ts | 14 +- .../org/team_details_ctrl_BACKUP_16633.ts | 127 ++++++++++++++ .../org/team_details_ctrl_BASE_16633.ts | 77 ++++++++ .../org/team_details_ctrl_LOCAL_16633.ts | 79 +++++++++ .../org/team_details_ctrl_REMOTE_16633.ts | 90 ++++++++++ public/sass/_grafana.scss | 3 +- public/sass/components/_form_dropdown.scss | 165 ++++++++++++++++++ public/sass/components/_user-picker.scss | 28 +++ 13 files changed, 759 insertions(+), 6 deletions(-) create mode 100644 public/app/core/components/UserPicker/UserPicker.tsx create mode 100644 public/app/core/components/UserPicker/UserPickerOption.tsx create mode 100644 public/app/features/org/team_details_ctrl_BACKUP_16633.ts create mode 100644 public/app/features/org/team_details_ctrl_BASE_16633.ts create mode 100644 public/app/features/org/team_details_ctrl_LOCAL_16633.ts create mode 100644 public/app/features/org/team_details_ctrl_REMOTE_16633.ts create mode 100644 public/sass/components/_form_dropdown.scss create mode 100644 public/sass/components/_user-picker.scss diff --git a/package.json b/package.json index bf3bdf616a7..2705c34b7fb 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-grid-layout": "^0.16.1", + "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..0b3974e7966 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -4,6 +4,7 @@ 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"]); @@ -11,4 +12,9 @@ export function registerAngularDirectives() { react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]); react2AngularDirective("loginBackground", LoginBackground, []); react2AngularDirective("searchResult", SearchResult, []); + react2AngularDirective("userPickerr", UserPicker, [ + "backendSrv", + "teamId", + "refreshList" + ]); } diff --git a/public/app/core/components/UserPicker/UserPicker.tsx b/public/app/core/components/UserPicker/UserPicker.tsx new file mode 100644 index 00000000000..3ead5be682a --- /dev/null +++ b/public/app/core/components/UserPicker/UserPicker.tsx @@ -0,0 +1,118 @@ +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 + }; + } + + componentWillReceiveProps(nextProps) { + console.log("componentWillReceiveProps", nextProps); + } + + handleChange(user) { + console.log("user", 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.get() in the angular controller + this.toggleLoading(false); + // this.$scope.$broadcast('user-picker-reset'); // TODO? + }); + } + + 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.tsx b/public/app/core/components/UserPicker/UserPickerOption.tsx new file mode 100644 index 00000000000..145784127ee --- /dev/null +++ b/public/app/core/components/UserPicker/UserPickerOption.tsx @@ -0,0 +1,48 @@ +import React, { Component } from "react"; + +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/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html index 4835facb785..f91c29c3bb3 100644 --- a/public/app/features/org/partials/team_details.html +++ b/public/app/features/org/partials/team_details.html @@ -30,7 +30,14 @@
Add member - + +
+
+ +
+
+ Add member +
diff --git a/public/app/features/org/team_details_ctrl.ts b/public/app/features/org/team_details_ctrl.ts index 0147e4d3d28..e2ae33491bb 100644 --- a/public/app/features/org/team_details_ctrl.ts +++ b/public/app/features/org/team_details_ctrl.ts @@ -1,4 +1,4 @@ -import coreModule from 'app/core/core_module'; +import coreModule from "app/core/core_module"; export default class TeamDetailsCtrl { team: Team; @@ -6,8 +6,14 @@ export default class TeamDetailsCtrl { navModel: any; /** @ngInject **/ - constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) { - this.navModel = navModelSrv.getNav('cfg', 'teams', 0); + 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 +41,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() { diff --git a/public/app/features/org/team_details_ctrl_BACKUP_16633.ts b/public/app/features/org/team_details_ctrl_BACKUP_16633.ts new file mode 100644 index 00000000000..ed98f4c40ff --- /dev/null +++ b/public/app/features/org/team_details_ctrl_BACKUP_16633.ts @@ -0,0 +1,127 @@ +import coreModule from "app/core/core_module"; + +export default class TeamDetailsCtrl { + team: Team; + teamMembers: User[] = []; + navModel: any; + + /** @ngInject **/ + constructor( + private $scope, + private backendSrv, + private $routeParams, + navModelSrv + ) { + this.navModel = navModelSrv.getNav("cfg", "teams", 0); + this.get = this.get.bind(this); + this.get(); + } + + get() { + if (this.$routeParams && this.$routeParams.id) { + this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => { + this.team = result; + }); +<<<<<<< HEAD + this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => { + this.teamMembers = result; + }); +======= + this.backendSrv + .get(`/api/teams/${this.$routeParams.id}/members`) + .then(result => { + this.teamMembers = result; + }); +>>>>>>> ux: POC on new select box for the user picker (#10289) + } + } + + removeTeamMember(teamMember: TeamMember) { +<<<<<<< HEAD + this.$scope.appEvent('confirm-modal', { + title: 'Remove Member', + text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?', + yesText: 'Remove', + icon: 'fa-warning', +======= + this.$scope.appEvent("confirm-modal", { + title: "Remove Member", + text: + "Are you sure you want to remove " + + teamMember.login + + " from this group?", + yesText: "Remove", + icon: "fa-warning", +>>>>>>> ux: POC on new select box for the user picker (#10289) + onConfirm: () => { + this.removeMemberConfirmed(teamMember); + }, + }); + } + + removeMemberConfirmed(teamMember: TeamMember) { +<<<<<<< HEAD + 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); +>>>>>>> ux: POC on new select box for the user picker (#10289) + } + + update() { + if (!this.$scope.teamDetailsForm.$valid) { + return; + } + +<<<<<<< HEAD + this.backendSrv.put('/api/teams/' + this.team.id, { + name: this.team.name, + email: this.team.email, + }); + } + + userPicked(user) { + this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }).then(() => { + this.$scope.$broadcast('user-picker-reset'); + this.get(); + }); +======= + this.backendSrv.put("/api/teams/" + this.team.id, { name: this.team.name }); + } + + userPicked(user) { + this.backendSrv + .post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }) + .then(() => { + this.$scope.$broadcast("user-picker-reset"); + this.get(); + }); +>>>>>>> ux: POC on new select box for the user picker (#10289) + } +} + +export interface Team { + id: number; + name: string; + email: string; +} + +export interface User { + id: number; + name: string; + login: string; + email: string; +} + +export interface TeamMember { + userId: number; + name: string; + login: string; +} + +<<<<<<< HEAD +coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl); +======= +coreModule.controller("TeamDetailsCtrl", TeamDetailsCtrl); +>>>>>>> ux: POC on new select box for the user picker (#10289) diff --git a/public/app/features/org/team_details_ctrl_BASE_16633.ts b/public/app/features/org/team_details_ctrl_BASE_16633.ts new file mode 100644 index 00000000000..e96c3512180 --- /dev/null +++ b/public/app/features/org/team_details_ctrl_BASE_16633.ts @@ -0,0 +1,77 @@ +import coreModule from 'app/core/core_module'; + +export default class TeamDetailsCtrl { + team: Team; + teamMembers: User[] = []; + navModel: any; + + /** @ngInject **/ + constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) { + this.navModel = navModelSrv.getNav('cfg', 'teams', 0); + this.get(); + } + + get() { + if (this.$routeParams && this.$routeParams.id) { + this.backendSrv.get(`/api/teams/${this.$routeParams.id}`) + .then(result => { + this.team = result; + }); + this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`) + .then(result => { + this.teamMembers = result; + }); + } + } + + removeTeamMember(teamMember: TeamMember) { + this.$scope.appEvent('confirm-modal', { + title: 'Remove Member', + text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?', + yesText: "Remove", + icon: "fa-warning", + onConfirm: () => { + this.removeMemberConfirmed(teamMember); + } + }); + } + + removeMemberConfirmed(teamMember: TeamMember) { + this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`) + .then(this.get.bind(this)); + } + + update() { + if (!this.$scope.teamDetailsForm.$valid) { return; } + + this.backendSrv.put('/api/teams/' + this.team.id, {name: this.team.name}); + } + + userPicked(user) { + this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, {userId: user.id}).then(() => { + this.$scope.$broadcast('user-picker-reset'); + this.get(); + }); + } +} + +export interface Team { + id: number; + name: string; +} + +export interface User { + id: number; + name: string; + login: string; + email: string; +} + +export interface TeamMember { + userId: number; + name: string; + login: string; +} + +coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl); + diff --git a/public/app/features/org/team_details_ctrl_LOCAL_16633.ts b/public/app/features/org/team_details_ctrl_LOCAL_16633.ts new file mode 100644 index 00000000000..0147e4d3d28 --- /dev/null +++ b/public/app/features/org/team_details_ctrl_LOCAL_16633.ts @@ -0,0 +1,79 @@ +import coreModule from 'app/core/core_module'; + +export default class TeamDetailsCtrl { + team: Team; + teamMembers: User[] = []; + navModel: any; + + /** @ngInject **/ + constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) { + this.navModel = navModelSrv.getNav('cfg', 'teams', 0); + this.get(); + } + + get() { + if (this.$routeParams && this.$routeParams.id) { + this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => { + this.team = result; + }); + this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => { + this.teamMembers = result; + }); + } + } + + removeTeamMember(teamMember: TeamMember) { + this.$scope.appEvent('confirm-modal', { + title: 'Remove Member', + text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?', + yesText: 'Remove', + icon: 'fa-warning', + onConfirm: () => { + this.removeMemberConfirmed(teamMember); + }, + }); + } + + removeMemberConfirmed(teamMember: TeamMember) { + this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get.bind(this)); + } + + update() { + if (!this.$scope.teamDetailsForm.$valid) { + return; + } + + this.backendSrv.put('/api/teams/' + this.team.id, { + name: this.team.name, + email: this.team.email, + }); + } + + userPicked(user) { + this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }).then(() => { + this.$scope.$broadcast('user-picker-reset'); + this.get(); + }); + } +} + +export interface Team { + id: number; + name: string; + email: string; +} + +export interface User { + id: number; + name: string; + login: string; + email: string; +} + +export interface TeamMember { + userId: number; + name: string; + login: string; +} + +coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl); diff --git a/public/app/features/org/team_details_ctrl_REMOTE_16633.ts b/public/app/features/org/team_details_ctrl_REMOTE_16633.ts new file mode 100644 index 00000000000..f623d2941b1 --- /dev/null +++ b/public/app/features/org/team_details_ctrl_REMOTE_16633.ts @@ -0,0 +1,90 @@ +import coreModule from "app/core/core_module"; + +export default class TeamDetailsCtrl { + team: Team; + teamMembers: User[] = []; + navModel: any; + + /** @ngInject **/ + constructor( + private $scope, + private backendSrv, + private $routeParams, + navModelSrv + ) { + this.navModel = navModelSrv.getNav("cfg", "teams", 0); + this.get = this.get.bind(this); + this.get(); + } + + get() { + if (this.$routeParams && this.$routeParams.id) { + this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => { + this.team = result; + }); + this.backendSrv + .get(`/api/teams/${this.$routeParams.id}/members`) + .then(result => { + this.teamMembers = result; + }); + } + } + + removeTeamMember(teamMember: TeamMember) { + this.$scope.appEvent("confirm-modal", { + title: "Remove Member", + text: + "Are you sure you want to remove " + + teamMember.login + + " from this group?", + yesText: "Remove", + icon: "fa-warning", + onConfirm: () => { + this.removeMemberConfirmed(teamMember); + } + }); + } + + removeMemberConfirmed(teamMember: TeamMember) { + this.backendSrv + .delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`) + .then(this.get); + } + + update() { + if (!this.$scope.teamDetailsForm.$valid) { + return; + } + + this.backendSrv.put("/api/teams/" + this.team.id, { name: this.team.name }); + } + + userPicked(user) { + this.backendSrv + .post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }) + .then(() => { + this.$scope.$broadcast("user-picker-reset"); + this.get(); + }); + } +} + +export interface Team { + id: number; + name: string; +} + +export interface User { + id: number; + name: string; + login: string; + email: string; +} + +export interface TeamMember { + userId: number; + name: string; + login: string; +} + +coreModule.controller("TeamDetailsCtrl", TeamDetailsCtrl); diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 3232e9c7f8e..a973fae1153 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -88,7 +88,8 @@ @import "components/page_header"; @import "components/dashboard_settings"; @import "components/empty_list_cta"; - +@import "components/user-picker"; +@import "components/form_dropdown"; // PAGES @import "pages/login"; @import "pages/dashboard"; diff --git a/public/sass/components/_form_dropdown.scss b/public/sass/components/_form_dropdown.scss new file mode 100644 index 00000000000..a3572afe679 --- /dev/null +++ b/public/sass/components/_form_dropdown.scss @@ -0,0 +1,165 @@ +$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; +@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-dropdown { + +// } + +.gf-form-input--form-dropdown { + padding: 0; + border: 0; + overflow: visible; + + .Select-placeholder { + color: #d8d9da; + } + + > .Select-control { + @include select-control(); + border-color: #262628; + } + + &.is-open > .Select-control { + background: transparent; + border-color: #262628; + } + + &.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% + ); + } + } +} + +// gf-form-input--dropdown + +// @mixin select-control() { +// width: 100%; +// margin-right: $gf-form-margin; +// @include border-radius($input-border-radius-sm); +// } + +// @mixin select-control-focus() { +// border-color: $input-border-focus; +// outline: none; +// $shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px $input-box-shadow-focus; +// @include box-shadow($shadow); +// } + +// .gf-form-dropdown-react { +// padding: 0px; +// } + +// .Select { +// &.is-focused > .Select-control { +// background-color: $input-bg; +// @include select-control-focus(); +// } +// &.is-focused:not(.is-open)>.Select-control, +// &.is-focused:not(.is-open).is-pseudo-focused>.Select-control { +// background-color: $input-label-bg; +// border: none; +// box-shadow: none; +// } +// &.has-value.Select--single>.Select-control .Select-value, +// &.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value { +// .Select-value-label { +// color: $input-color; +// } +// } +// } + +// .gf-form-label { + +// .Select-control { +// @include select-control(); +// font-size: $font-size-sm; +// color: $input-color; +// background-color: $input-label-bg; // padding: $input-padding-y $input-padding-x; +// display: block; +// border: $input-btn-border-width solid transparent; +// } +// } + +// .gf-form-input { +// overflow: visible; + +// .Select-control { +// @include select-control(); +// background-color: $input-bg; +// color: $input-color; +// border: $input-btn-border-width solid $input-border-color; +// } +// } + +// .Select-menu-outer { +// margin: 2px 0 0; +// border: 1px solid $dropdownBorder; +// background: $dropdownBackground; +// @include border-radius($input-border-radius-sm); +// } + +// .Select-option { +// font-size: $font-size-sm; +// padding: 3px 20px 3px 15px; +// &:last-child { +// @include border-radius($input-border-radius-sm); +// } +// &.is-selected { +// background-color: $dropdownLinkBackgroundHover; +// color: $dropdownLinkColorHover; +// } +// &.is-focused { +// background-color: $dropdownLinkBackgroundHover; +// color: $dropdownLinkColorHover; +// } +// &.is-disabled { +// color: $gray-2; +// } +// } diff --git a/public/sass/components/_user-picker.scss b/public/sass/components/_user-picker.scss new file mode 100644 index 00000000000..f61dafc54c6 --- /dev/null +++ b/public/sass/components/_user-picker.scss @@ -0,0 +1,28 @@ +.user-picker-option__button { + position: relative; + text-align: left; + width: 100%; + background-color: #171819; + display: block; + border-radius: 0; + + // &:hover { + // background-color: #262628; + // // background-color: blue; + // &::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%); + // } + // } +} +.user-picker-option__avatar { + width: 20px; + display: inline-block; + margin-right: 10px; +}