Commit e3e2cd82 by Shavonn Brown Committed by GitHub

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
parent 50058de6
...@@ -11,6 +11,7 @@ import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField, DataLi ...@@ -11,6 +11,7 @@ import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField, DataLi
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor'; import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
import { SearchField } from './components/search/SearchField'; import { SearchField } from './components/search/SearchField';
import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu'; import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu';
import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('sidemenu', SideMenu, []); react2AngularDirective('sidemenu', SideMenu, []);
...@@ -87,4 +88,6 @@ export function registerAngularDirectives() { ...@@ -87,4 +88,6 @@ export function registerAngularDirectives() {
'suggestions', 'suggestions',
['onChange', { watchDepth: 'reference', wrapApply: true }], ['onChange', { watchDepth: 'reference', wrapApply: true }],
]); ]);
react2AngularDirective('reactProfileWrapper', ReactProfileWrapper, []);
} }
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { User } from 'app/types';
export interface UserAPI { export interface UserAPI {
changePassword: (ChangePassword: ChangePasswordFields) => void; changePassword: (changePassword: ChangePasswordFields) => void;
updateUserProfile: (profile: ProfileUpdateFields) => void;
loadUser: () => void;
} }
interface LoadingStates { interface LoadingStates {
changePassword: boolean; changePassword: boolean;
loadUser: boolean;
updateUserProfile: boolean;
} }
export interface ChangePasswordFields { export interface ChangePasswordFields {
...@@ -15,11 +20,19 @@ export interface ChangePasswordFields { ...@@ -15,11 +20,19 @@ export interface ChangePasswordFields {
confirmNew: string; confirmNew: string;
} }
export interface ProfileUpdateFields {
name: string;
email: string;
login: string;
}
export interface Props { 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 { export interface State {
user?: User;
loadingStates: LoadingStates; loadingStates: LoadingStates;
} }
...@@ -27,24 +40,55 @@ export class UserProvider extends PureComponent<Props, State> { ...@@ -27,24 +40,55 @@ export class UserProvider extends PureComponent<Props, State> {
state: State = { state: State = {
loadingStates: { loadingStates: {
changePassword: false, changePassword: false,
loadUser: true,
updateUserProfile: false,
}, },
}; };
componentDidMount() {
if (this.props.userId) {
this.loadUser();
}
}
changePassword = async (payload: ChangePasswordFields) => { changePassword = async (payload: ChangePasswordFields) => {
this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: true } }); this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: true } });
await getBackendSrv().put('/api/user/password', payload); await getBackendSrv().put('/api/user/password', payload);
this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: false } }); 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() { render() {
const { children } = this.props; const { children } = this.props;
const { loadingStates } = this.state; const { loadingStates, user } = this.state;
const api = { const api = {
changePassword: this.changePassword, changePassword: this.changePassword,
loadUser: this.loadUser,
updateUserProfile: this.updateUserProfile,
}; };
return <>{children(api, loadingStates)}</>; return <>{children(api, loadingStates, user)}</>;
} }
} }
......
...@@ -16,15 +16,11 @@ export interface State { ...@@ -16,15 +16,11 @@ export interface State {
} }
export class ChangePasswordForm extends PureComponent<Props, State> { export class ChangePasswordForm extends PureComponent<Props, State> {
constructor(props: Props) { state: State = {
super(props); oldPassword: '',
newPassword: '',
this.state = { confirmNew: '',
oldPassword: '', };
newPassword: '',
confirmNew: '',
};
}
onOldPasswordChange = (oldPassword: string) => { onOldPasswordChange = (oldPassword: string) => {
this.setState({ oldPassword }); this.setState({ oldPassword });
......
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
react2AngularDirective('prefsControl', SharedPreferences, ['resourceUri']);
...@@ -3,7 +3,6 @@ import { coreModule, NavModelSrv } from 'app/core/core'; ...@@ -3,7 +3,6 @@ import { coreModule, NavModelSrv } from 'app/core/core';
import { dateTime } from '@grafana/data'; import { dateTime } from '@grafana/data';
import { UserSession } from 'app/types'; import { UserSession } from 'app/types';
import { BackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv } from 'app/core/services/backend_srv';
import { ILocationService } from 'angular';
export class ProfileCtrl { export class ProfileCtrl {
user: any; user: any;
...@@ -18,26 +17,13 @@ export class ProfileCtrl { ...@@ -18,26 +17,13 @@ export class ProfileCtrl {
navModel: any; navModel: any;
/** @ngInject */ /** @ngInject */
constructor( constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
private backendSrv: BackendSrv,
private contextSrv: any,
private $location: ILocationService,
navModelSrv: NavModelSrv
) {
this.getUser();
this.getUserSessions(); this.getUserSessions();
this.getUserTeams(); this.getUserTeams();
this.getUserOrgs(); this.getUserOrgs();
this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0); 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() { getUserSessions() {
this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => { this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => {
sessions.reverse(); sessions.reverse();
...@@ -103,19 +89,6 @@ export class ProfileCtrl { ...@@ -103,19 +89,6 @@ export class ProfileCtrl {
window.location.href = config.appSubUrl + '/profile'; 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); coreModule.controller('ProfileCtrl', ProfileCtrl);
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;
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;
import './ProfileCtrl'; import './ProfileCtrl';
import './PrefControlCtrl';
<page-header model="ctrl.navModel"></page-header> <page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body"> <div class="page-container page-body">
<h3 class="page-sub-heading">User Profile</h3> <react-profile-wrapper></react-profile-wrapper>
<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>
<h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3> <h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3>
<div class="gf-form-group" ng-show="ctrl.showTeamsList"> <div class="gf-form-group" ng-show="ctrl.showTeamsList">
......
...@@ -67,6 +67,8 @@ describe('Functions', () => { ...@@ -67,6 +67,8 @@ describe('Functions', () => {
label: '', label: '',
avatarUrl: '', avatarUrl: '',
login: '', login: '',
name: '',
email: '',
}; };
instance.onAddUserToTeam(); instance.onAddUserToTeam();
......
...@@ -16,6 +16,8 @@ export interface User { ...@@ -16,6 +16,8 @@ export interface User {
label: string; label: string;
avatarUrl: string; avatarUrl: string;
login: string; login: string;
email: string;
name: string;
} }
export interface Invitee { export interface Invitee {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment