浏览代码

Rewrite user profile edit to react (#17917)

* rewrite user profile edit to react (#17525)

* disableLogin change, still need to fix tooltip

* left out disable form for other auth

* PR changes - wrapper to render, userId instead of bool, optional user in state, change provider child param order

* moved directive to angular_wrappers

* catch api error

* finally

* move user arg back to end- optional

* optional type sig
Shavonn Brown 6 年之前
父节点
当前提交
e3e2cd82d7

+ 3 - 0
public/app/core/angular_wrappers.ts

@@ -11,6 +11,7 @@ import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField, DataLi
 import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
 import { SearchField } from './components/search/SearchField';
 import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu';
+import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
 
 export function registerAngularDirectives() {
   react2AngularDirective('sidemenu', SideMenu, []);
@@ -87,4 +88,6 @@ export function registerAngularDirectives() {
     'suggestions',
     ['onChange', { watchDepth: 'reference', wrapApply: true }],
   ]);
+
+  react2AngularDirective('reactProfileWrapper', ReactProfileWrapper, []);
 }

+ 48 - 4
public/app/core/utils/UserProvider.tsx

@@ -1,12 +1,17 @@
 import React, { PureComponent } from 'react';
 import { getBackendSrv } from '@grafana/runtime';
+import { User } from 'app/types';
 
 export interface UserAPI {
-  changePassword: (ChangePassword: ChangePasswordFields) => void;
+  changePassword: (changePassword: ChangePasswordFields) => void;
+  updateUserProfile: (profile: ProfileUpdateFields) => void;
+  loadUser: () => void;
 }
 
 interface LoadingStates {
   changePassword: boolean;
+  loadUser: boolean;
+  updateUserProfile: boolean;
 }
 
 export interface ChangePasswordFields {
@@ -15,11 +20,19 @@ export interface ChangePasswordFields {
   confirmNew: string;
 }
 
+export interface ProfileUpdateFields {
+  name: string;
+  email: string;
+  login: string;
+}
+
 export interface Props {
-  children: (api: UserAPI, states: LoadingStates) => JSX.Element;
+  userId?: number; // passed, will load user on mount
+  children: (api: UserAPI, states: LoadingStates, user?: User) => JSX.Element;
 }
 
 export interface State {
+  user?: User;
   loadingStates: LoadingStates;
 }
 
@@ -27,24 +40,55 @@ export class UserProvider extends PureComponent<Props, State> {
   state: State = {
     loadingStates: {
       changePassword: false,
+      loadUser: true,
+      updateUserProfile: false,
     },
   };
 
+  componentDidMount() {
+    if (this.props.userId) {
+      this.loadUser();
+    }
+  }
+
   changePassword = async (payload: ChangePasswordFields) => {
     this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: true } });
     await getBackendSrv().put('/api/user/password', payload);
     this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: false } });
   };
 
+  loadUser = async () => {
+    this.setState({
+      loadingStates: { ...this.state.loadingStates, loadUser: true },
+    });
+    const user = await getBackendSrv().get('/api/user');
+    this.setState({ user, loadingStates: { ...this.state.loadingStates, loadUser: Object.keys(user).length === 0 } });
+  };
+
+  updateUserProfile = async (payload: ProfileUpdateFields) => {
+    this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: true } });
+    await getBackendSrv()
+      .put('/api/user', payload)
+      .then(() => {
+        this.loadUser();
+      })
+      .catch(e => console.log(e))
+      .finally(() => {
+        this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: false } });
+      });
+  };
+
   render() {
     const { children } = this.props;
-    const { loadingStates } = this.state;
+    const { loadingStates, user } = this.state;
 
     const api = {
       changePassword: this.changePassword,
+      loadUser: this.loadUser,
+      updateUserProfile: this.updateUserProfile,
     };
 
-    return <>{children(api, loadingStates)}</>;
+    return <>{children(api, loadingStates, user)}</>;
   }
 }
 

+ 5 - 9
public/app/features/profile/ChangePasswordForm.tsx

@@ -16,15 +16,11 @@ export interface State {
 }
 
 export class ChangePasswordForm extends PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      oldPassword: '',
-      newPassword: '',
-      confirmNew: '',
-    };
-  }
+  state: State = {
+    oldPassword: '',
+    newPassword: '',
+    confirmNew: '',
+  };
 
   onOldPasswordChange = (oldPassword: string) => {
     this.setState({ oldPassword });

+ 0 - 4
public/app/features/profile/PrefControlCtrl.ts

@@ -1,4 +0,0 @@
-import { react2AngularDirective } from 'app/core/utils/react2angular';
-import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
-
-react2AngularDirective('prefsControl', SharedPreferences, ['resourceUri']);

+ 1 - 28
public/app/features/profile/ProfileCtrl.ts

@@ -3,7 +3,6 @@ import { coreModule, NavModelSrv } from 'app/core/core';
 import { dateTime } from '@grafana/data';
 import { UserSession } from 'app/types';
 import { BackendSrv } from 'app/core/services/backend_srv';
-import { ILocationService } from 'angular';
 
 export class ProfileCtrl {
   user: any;
@@ -18,26 +17,13 @@ export class ProfileCtrl {
   navModel: any;
 
   /** @ngInject */
-  constructor(
-    private backendSrv: BackendSrv,
-    private contextSrv: any,
-    private $location: ILocationService,
-    navModelSrv: NavModelSrv
-  ) {
-    this.getUser();
+  constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
     this.getUserSessions();
     this.getUserTeams();
     this.getUserOrgs();
     this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0);
   }
 
-  getUser() {
-    this.backendSrv.get('/api/user').then((user: any) => {
-      this.user = user;
-      this.user.theme = user.theme || 'dark';
-    });
-  }
-
   getUserSessions() {
     this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => {
       sessions.reverse();
@@ -103,19 +89,6 @@ export class ProfileCtrl {
       window.location.href = config.appSubUrl + '/profile';
     });
   }
-
-  update() {
-    if (!this.userForm.$valid) {
-      return;
-    }
-
-    this.backendSrv.put('/api/user/', this.user).then(() => {
-      this.contextSrv.user.name = this.user.name || this.user.login;
-      if (this.oldTheme !== this.user.theme) {
-        window.location.href = config.appSubUrl + this.$location.path();
-      }
-    });
-  }
 }
 
 coreModule.controller('ProfileCtrl', ProfileCtrl);

+ 26 - 0
public/app/features/profile/ReactProfileWrapper.tsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import { UserProvider } from 'app/core/utils/UserProvider';
+import { UserProfileEditForm } from './UserProfileEditForm';
+import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
+import { config } from '@grafana/runtime';
+
+export const ReactProfileWrapper = () => (
+  <UserProvider userId={config.bootData.user.id}>
+    {(api, states, user) => {
+      return (
+        <>
+          {!states.loadUser && (
+            <UserProfileEditForm
+              updateProfile={api.updateUserProfile}
+              isSavingUser={states.updateUserProfile}
+              user={user}
+            />
+          )}
+          <SharedPreferences resourceUri="user" />
+        </>
+      );
+    }}
+  </UserProvider>
+);
+
+export default ReactProfileWrapper;

+ 105 - 0
public/app/features/profile/UserProfileEditForm.tsx

@@ -0,0 +1,105 @@
+import React, { PureComponent, ChangeEvent, MouseEvent } from 'react';
+import { Button, FormLabel, Input, Tooltip } from '@grafana/ui';
+import { User } from 'app/types';
+import config from 'app/core/config';
+import { ProfileUpdateFields } from 'app/core/utils/UserProvider';
+
+export interface Props {
+  user: User;
+  isSavingUser: boolean;
+  updateProfile: (payload: ProfileUpdateFields) => void;
+}
+
+export interface State {
+  name: string;
+  email: string;
+  login: string;
+}
+
+export class UserProfileEditForm extends PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    const {
+      user: { name, email, login },
+    } = this.props;
+
+    this.state = {
+      name,
+      email,
+      login,
+    };
+  }
+
+  onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.setState({ name: event.target.value });
+  };
+
+  onEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.setState({ email: event.target.value });
+  };
+
+  onLoginChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.setState({ login: event.target.value });
+  };
+
+  onSubmitProfileUpdate = (event: MouseEvent<HTMLInputElement>) => {
+    event.preventDefault();
+    this.props.updateProfile({ ...this.state });
+  };
+
+  render() {
+    const { name, email, login } = this.state;
+    const { isSavingUser } = this.props;
+    const { disableLoginForm } = config;
+
+    return (
+      <>
+        <h3 className="page-sub-heading">Edit Profile</h3>
+        <form name="userForm" className="gf-form-group">
+          <div className="gf-form max-width-30">
+            <FormLabel className="width-8">Name</FormLabel>
+            <Input className="gf-form-input max-width-22" type="text" onChange={this.onNameChange} value={name} />
+          </div>
+          <div className="gf-form max-width-30">
+            <FormLabel className="width-8">Email</FormLabel>
+            <Input
+              className="gf-form-input max-width-22"
+              type="text"
+              onChange={this.onEmailChange}
+              value={email}
+              disabled={disableLoginForm}
+            />
+            {disableLoginForm && (
+              <Tooltip content="Login Details Locked - managed in another system.">
+                <i className="fa fa-lock gf-form-icon--right-absolute" />
+              </Tooltip>
+            )}
+          </div>
+          <div className="gf-form max-width-30">
+            <FormLabel className="width-8">Username</FormLabel>
+            <Input
+              className="gf-form-input max-width-22"
+              type="text"
+              onChange={this.onLoginChange}
+              value={login}
+              disabled={disableLoginForm}
+            />
+            {disableLoginForm && (
+              <Tooltip content="Login Details Locked - managed in another system.">
+                <i className="fa fa-lock gf-form-icon--right-absolute" />
+              </Tooltip>
+            )}
+          </div>
+          <div className="gf-form-button-row">
+            <Button variant="primary" onClick={this.onSubmitProfileUpdate} disabled={isSavingUser}>
+              Save
+            </Button>
+          </div>
+        </form>
+      </>
+    );
+  }
+}
+
+export default UserProfileEditForm;

+ 0 - 1
public/app/features/profile/all.ts

@@ -1,2 +1 @@
 import './ProfileCtrl';
-import './PrefControlCtrl';

+ 1 - 43
public/app/features/profile/partials/profile.html

@@ -1,49 +1,7 @@
 <page-header model="ctrl.navModel"></page-header>
 
 <div class="page-container page-body">
-  <h3 class="page-sub-heading">User Profile</h3>
-
-  <form name="ctrl.userForm" class="gf-form-group">
-    <div class="gf-form max-width-30">
-      <span class="gf-form-label width-8">Name</span>
-      <input class="gf-form-input max-width-22" type="text" required ng-model="ctrl.user.name" />
-    </div>
-    <div class="gf-form max-width-30">
-      <span class="gf-form-label width-8">Email</span>
-      <input
-        class="gf-form-input max-width-22"
-        type="email"
-        ng-readonly="ctrl.readonlyLoginFields"
-        required
-        ng-model="ctrl.user.email"
-      />
-      <i
-        ng-if="ctrl.readonlyLoginFields"
-        class="fa fa-lock gf-form-icon--right-absolute"
-        bs-tooltip="'Login Details Locked - managed in another system.'"
-      ></i>
-    </div>
-    <div class="gf-form max-width-30">
-      <span class="gf-form-label width-8">Username</span>
-      <input
-        class="gf-form-input max-width-22"
-        type="text"
-        ng-readonly="ctrl.readonlyLoginFields"
-        required
-        ng-model="ctrl.user.login"
-      />
-      <i
-        ng-if="ctrl.readonlyLoginFields"
-        class="fa fa-lock gf-form-icon--right-absolute"
-        bs-tooltip="'Login Details Locked - managed in another system.'"
-      ></i>
-    </div>
-    <div class="gf-form-button-row">
-      <button type="submit" class="btn btn-primary" ng-click="ctrl.update()">Save</button>
-    </div>
-  </form>
-
-  <prefs-control resource-uri="'user'"></prefs-control>
+  <react-profile-wrapper></react-profile-wrapper>
 
   <h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3>
   <div class="gf-form-group" ng-show="ctrl.showTeamsList">

+ 2 - 0
public/app/features/teams/TeamMembers.test.tsx

@@ -67,6 +67,8 @@ describe('Functions', () => {
       label: '',
       avatarUrl: '',
       login: '',
+      name: '',
+      email: '',
     };
 
     instance.onAddUserToTeam();

+ 2 - 0
public/app/types/user.ts

@@ -16,6 +16,8 @@ export interface User {
   label: string;
   avatarUrl: string;
   login: string;
+  email: string;
+  name: string;
 }
 
 export interface Invitee {