From 874b8abcc0e3241bd6ba3d20861e429734e8ccac Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Wed, 3 Jul 2019 11:02:12 -0400 Subject: [PATCH] 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 --- .../src/components/Button/AbstractButton.tsx | 2 +- .../PasswordInput/PasswordInput.tsx | 20 +++++ public/app/core/utils/UserProvider.tsx | 51 ++++++++++++ .../features/profile/ChangePasswordCtrl.ts | 29 ------- .../features/profile/ChangePasswordForm.tsx | 79 +++++++++++++++++++ .../features/profile/ChangePasswordPage.tsx | 46 +++++++++++ public/app/features/profile/all.ts | 1 - .../profile/partials/change_password.html | 35 -------- public/app/routes/routes.ts | 7 +- 9 files changed, 202 insertions(+), 68 deletions(-) create mode 100644 public/app/core/components/PasswordInput/PasswordInput.tsx create mode 100644 public/app/core/utils/UserProvider.tsx delete mode 100644 public/app/features/profile/ChangePasswordCtrl.ts create mode 100644 public/app/features/profile/ChangePasswordForm.tsx create mode 100644 public/app/features/profile/ChangePasswordPage.tsx delete mode 100644 public/app/features/profile/partials/change_password.html diff --git a/packages/grafana-ui/src/components/Button/AbstractButton.tsx b/packages/grafana-ui/src/components/Button/AbstractButton.tsx index 0516234a70b..030d03fcdd2 100644 --- a/packages/grafana-ui/src/components/Button/AbstractButton.tsx +++ b/packages/grafana-ui/src/components/Button/AbstractButton.tsx @@ -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}; diff --git a/public/app/core/components/PasswordInput/PasswordInput.tsx b/public/app/core/components/PasswordInput/PasswordInput.tsx new file mode 100644 index 00000000000..cd485ed55f1 --- /dev/null +++ b/public/app/core/components/PasswordInput/PasswordInput.tsx @@ -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((props, ref) => ( + <> + {props.label} + ) => props.onChange(event.target.value)} + value={props.value} + /> + +)); diff --git a/public/app/core/utils/UserProvider.tsx b/public/app/core/utils/UserProvider.tsx new file mode 100644 index 00000000000..bf855f2879a --- /dev/null +++ b/public/app/core/utils/UserProvider.tsx @@ -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 { + 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; diff --git a/public/app/features/profile/ChangePasswordCtrl.ts b/public/app/features/profile/ChangePasswordCtrl.ts deleted file mode 100644 index 7a4ba0f031a..00000000000 --- a/public/app/features/profile/ChangePasswordCtrl.ts +++ /dev/null @@ -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); diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx new file mode 100644 index 00000000000..d88c9385795 --- /dev/null +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -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 { + 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) => { + 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

You cannot change password when ldap or auth proxy authentication is enabled.

; + } + + return ( +
+
+ +
+
+ +
+
+ +
+
+ + + Cancel + +
+
+ ); + } +} + +export default ChangePasswordForm; diff --git a/public/app/features/profile/ChangePasswordPage.tsx b/public/app/features/profile/ChangePasswordPage.tsx new file mode 100644 index 00000000000..85872c8dfba --- /dev/null +++ b/public/app/features/profile/ChangePasswordPage.tsx @@ -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 { + render() { + const { navModel } = this.props; + return ( + + + {({ changePassword }, states) => ( + +

Change Your Password

+ +
+ )} +
+
+ ); + } +} + +function mapStateToProps(state: StoreState) { + return { + navModel: getNavModel(state.navIndex, `change-password`), + }; +} + +const mapDispatchToProps = {}; + +export default hot(module)( + connect( + mapStateToProps, + mapDispatchToProps + )(ChangePasswordPage) +); diff --git a/public/app/features/profile/all.ts b/public/app/features/profile/all.ts index 6d462c08e6b..656ca1ddcfa 100644 --- a/public/app/features/profile/all.ts +++ b/public/app/features/profile/all.ts @@ -1,3 +1,2 @@ import './ProfileCtrl'; -import './ChangePasswordCtrl'; import './PrefControlCtrl'; diff --git a/public/app/features/profile/partials/change_password.html b/public/app/features/profile/partials/change_password.html deleted file mode 100644 index 49a45bd6653..00000000000 --- a/public/app/features/profile/partials/change_password.html +++ /dev/null @@ -1,35 +0,0 @@ - - -
-

- Change your password -

- -
- You cannot change password when ldap or auth proxy authentication is enabled. -
- -
-
- Old Password - -
- -
- New Password - -
- -
- Confirm Password - -
- -
- - Cancel -
-
- -
- diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 54722af5ba7..26746f82168 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -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: '', + resolve: { + component: () => ChangePasswordPage, + }, }) .when('/profile/select-org', { templateUrl: 'public/app/features/org/partials/select_org.html',