Browse Source

ux: POC on new select box for the user picker (#10289)

Johannes Schill 8 years ago
parent
commit
5931d6c87d

+ 1 - 0
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",

+ 6 - 0
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"
+  ]);
 }

+ 118 - 0
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<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
+    };
+  }
+
+  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 (
+      <div className="user-picker">
+        <AsyncComponent
+          valueKey="id"
+          multi={this.state.multi}
+          labelKey="label"
+          cache={false}
+          isLoading={this.state.isLoading}
+          loadOptions={this.debouncedSearchUsers}
+          loadingPlaceholder="Loading..."
+          onChange={this.handleChange}
+          className="width-8 gf-form-input gf-form-input--form-dropdown"
+          optionComponent={UserPickerOption}
+          placeholder="Choose"
+        />
+      </div>
+    );
+  }
+}
+
+export default UserPicker;

+ 48 - 0
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 (
+      <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;

+ 8 - 1
public/app/features/org/partials/team_details.html

@@ -30,7 +30,14 @@
     <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>
+				<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
+      </div>
+    </form>
+
+		<form name="ctrl.addMemberForm" class="gf-form-group">
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Add member</span>
+				<user-pickerr backendSrv="ctrl.backendSrv" teamId="ctrl.$routeParams.id" refreshList="ctrl.get" teamMembers="ctrl.teamMembers"></user-pickerr>
       </div>
     </form>
 

+ 10 - 4
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() {

+ 127 - 0
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)

+ 77 - 0
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);
+

+ 79 - 0
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);

+ 90 - 0
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);

+ 2 - 1
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";

+ 165 - 0
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;
+//   }
+// }

+ 28 - 0
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;
+}