ChangePassword: Rewrite change password page to react (#17811)

* ChangePassword to React, created PasswordInput component, attempting UserProvider wrapper component, adding flex to btn row

* UserAPI interface, force classes on PasswordInput, remove api call from ChangePassword

* refactored out form

* clean up

* removed unnecessary bind, added loading state and loading component to change password form

* should be OR

* arrow funcs instead of binds, inline-block instead of flex, isSaving instead of isLoading, disabled button instead of spinner

* inline-flex on the react btn

* change state instatiatiation
This commit is contained in:
Shavonn Brown 2019-07-03 11:02:12 -04:00 committed by Torkel Ödegaard
parent b1126cb0ed
commit 874b8abcc0
9 changed files with 202 additions and 68 deletions

View File

@ -120,7 +120,7 @@ const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonV
return {
button: css`
label: button;
display: flex;
display: inline-flex;
align-items: center;
font-weight: ${fontWeight};
font-size: ${fontSize};

View File

@ -0,0 +1,20 @@
import React, { ChangeEvent, forwardRef } from 'react';
import { Input, FormLabel } from '@grafana/ui';
export interface Props {
label: string;
value: string | undefined;
onChange: (value: string) => void;
}
export const PasswordInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
<>
<FormLabel className="width-8">{props.label}</FormLabel>
<Input
className="gf-form-input max-width-22"
type="password"
onChange={(event: ChangeEvent<HTMLInputElement>) => props.onChange(event.target.value)}
value={props.value}
/>
</>
));

View File

@ -0,0 +1,51 @@
import React, { PureComponent } from 'react';
import { getBackendSrv } from '@grafana/runtime';
export interface UserAPI {
changePassword: (ChangePassword: ChangePasswordFields) => void;
}
interface LoadingStates {
changePassword: boolean;
}
export interface ChangePasswordFields {
oldPassword: string;
newPassword: string;
confirmNew: string;
}
export interface Props {
children: (api: UserAPI, states: LoadingStates) => JSX.Element;
}
export interface State {
loadingStates: LoadingStates;
}
export class UserProvider extends PureComponent<Props, State> {
state: State = {
loadingStates: {
changePassword: false,
},
};
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 } });
};
render() {
const { children } = this.props;
const { loadingStates } = this.state;
const api = {
changePassword: this.changePassword,
};
return <>{children(api, loadingStates)}</>;
}
}
export default UserProvider;

View File

@ -1,29 +0,0 @@
import angular from 'angular';
import config from 'app/core/config';
export class ChangePasswordCtrl {
/** @ngInject */
constructor($scope, backendSrv, $location, navModelSrv) {
$scope.command = {};
$scope.authProxyEnabled = config.authProxyEnabled;
$scope.ldapEnabled = config.ldapEnabled;
$scope.navModel = navModelSrv.getNav('profile', 'change-password', 0);
$scope.changePassword = () => {
if (!$scope.userForm.$valid) {
return;
}
if ($scope.command.newPassword !== $scope.command.confirmNew) {
$scope.appEvent('alert-warning', ['New passwords do not match', '']);
return;
}
backendSrv.put('/api/user/password', $scope.command).then(() => {
$location.path('profile');
});
};
}
}
angular.module('grafana.controllers').controller('ChangePasswordCtrl', ChangePasswordCtrl);

View File

@ -0,0 +1,79 @@
import React, { PureComponent, MouseEvent } from 'react';
import config from 'app/core/config';
import { Button, LinkButton } from '@grafana/ui';
import { ChangePasswordFields } from 'app/core/utils/UserProvider';
import { PasswordInput } from 'app/core/components/PasswordInput/PasswordInput';
export interface Props {
isSaving: boolean;
onChangePassword: (payload: ChangePasswordFields) => void;
}
export interface State {
oldPassword: string;
newPassword: string;
confirmNew: string;
}
export class ChangePasswordForm extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
oldPassword: '',
newPassword: '',
confirmNew: '',
};
}
onOldPasswordChange = (oldPassword: string) => {
this.setState({ oldPassword });
};
onNewPasswordChange = (newPassword: string) => {
this.setState({ newPassword });
};
onConfirmPasswordChange = (confirmNew: string) => {
this.setState({ confirmNew });
};
onSubmitChangePassword = (event: MouseEvent<HTMLInputElement>) => {
event.preventDefault();
this.props.onChangePassword({ ...this.state });
};
render() {
const { oldPassword, newPassword, confirmNew } = this.state;
const { isSaving } = this.props;
const { ldapEnabled, authProxyEnabled } = config;
if (ldapEnabled || authProxyEnabled) {
return <p>You cannot change password when ldap or auth proxy authentication is enabled.</p>;
}
return (
<form name="userForm" className="gf-form-group">
<div className="gf-form max-width-30">
<PasswordInput label="Old Password" onChange={this.onOldPasswordChange} value={oldPassword} />
</div>
<div className="gf-form max-width-30">
<PasswordInput label="New Password" onChange={this.onNewPasswordChange} value={newPassword} />
</div>
<div className="gf-form max-width-30">
<PasswordInput label="Confirm Password" onChange={this.onConfirmPasswordChange} value={confirmNew} />
</div>
<div className="gf-form-button-row">
<Button variant="primary" onClick={this.onSubmitChangePassword} disabled={isSaving}>
Change Password
</Button>
<LinkButton variant="transparent" href={`${config.appSubUrl}/profile`}>
Cancel
</LinkButton>
</div>
</form>
);
}
}
export default ChangePasswordForm;

View File

@ -0,0 +1,46 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { StoreState } from 'app/types';
import { NavModel } from '@grafana/data';
import { getNavModel } from 'app/core/selectors/navModel';
import { UserProvider } from 'app/core/utils/UserProvider';
import Page from 'app/core/components/Page/Page';
import { ChangePasswordForm } from './ChangePasswordForm';
export interface Props {
navModel: NavModel;
}
export class ChangePasswordPage extends PureComponent<Props> {
render() {
const { navModel } = this.props;
return (
<Page navModel={navModel}>
<UserProvider>
{({ changePassword }, states) => (
<Page.Contents>
<h3 className="page-sub-heading">Change Your Password</h3>
<ChangePasswordForm onChangePassword={changePassword} isSaving={states.changePassword} />
</Page.Contents>
)}
</UserProvider>
</Page>
);
}
}
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, `change-password`),
};
}
const mapDispatchToProps = {};
export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps
)(ChangePasswordPage)
);

View File

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

View File

@ -1,35 +0,0 @@
<page-header model="navModel"></page-header>
<div class="page-container page-body">
<h3 class="page-sub-heading">
Change your password
</h3>
<div ng-if="ldapEnabled || authProxyEnabled">
You cannot change password when ldap or auth proxy authentication is enabled.
</div>
<form name="userForm" class="gf-form-group" ng-hide="ldapEnabled || authProxyEnabled">
<div class="gf-form">
<span class="gf-form-label width-10">Old Password</span>
<input class="gf-form-input max-width-21" type="password" required ng-model="command.oldPassword">
</div>
<div class="gf-form">
<span class="gf-form-label width-10">New Password</span>
<input class="gf-form-input max-width-21" type="password" required ng-minlength="4" ng-model="command.newPassword">
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Confirm Password</span>
<input class="gf-form-input max-width-21" type="password" required ng-minlength="4" ng-model="command.confirmNew">
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-primary" ng-click="changePassword()">Change Password</button>
<a class="btn-text" href="profile">Cancel</a>
</div>
</form>
</div>

View File

@ -3,6 +3,7 @@ import './ReactContainer';
import { applyRouteRegistrationHandlers } from './registry';
// Pages
import ChangePasswordPage from 'app/features/profile/ChangePasswordPage';
import ServerStats from 'app/features/admin/ServerStats';
import AlertRuleList from 'app/features/alerting/AlertRuleList';
import TeamPages from 'app/features/teams/TeamPages';
@ -230,8 +231,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
controllerAs: 'ctrl',
})
.when('/profile/password', {
templateUrl: 'public/app/features/profile/partials/change_password.html',
controller: 'ChangePasswordCtrl',
template: '<react-container />',
resolve: {
component: () => ChangePasswordPage,
},
})
.when('/profile/select-org', {
templateUrl: 'public/app/features/org/partials/select_org.html',