|
1 | | -import * as React from 'react'; |
| 1 | +import React, {useCallback, useContext, useRef, useState} from 'react'; |
2 | 2 |
|
3 | 3 | import {FormField, NotificationType, SlidingPanel} from 'argo-ui/src/index'; |
4 | | -import * as PropTypes from 'prop-types'; |
5 | 4 | import {Form, FormApi, FormValue, Nested, Text} from 'react-form'; |
6 | | -import {RouteComponentProps} from 'react-router'; |
7 | 5 | import {DataLoader, ErrorNotification, Page, Spinner} from '../../../shared/components'; |
8 | | -import {AppContext} from '../../../shared/context'; |
| 6 | +import {Context} from '../../../shared/context'; |
9 | 7 | import {services} from '../../../shared/services'; |
10 | 8 |
|
11 | 9 | import './user-info-overview.scss'; |
| 10 | +import {UserInfo} from '../../../shared/models'; |
12 | 11 |
|
13 | | -export class UserInfoOverview extends React.Component<RouteComponentProps<any>, {connecting: boolean}> { |
14 | | - public static contextTypes = { |
15 | | - router: PropTypes.object, |
16 | | - apis: PropTypes.object, |
17 | | - history: PropTypes.object |
18 | | - }; |
| 12 | +// Constants |
| 13 | +const CHANGE_PASSWORD_PARAM = 'changePassword'; |
| 14 | +const ARGOCD_ISSUER = 'argocd'; |
19 | 15 |
|
20 | | - private formApiPassword: FormApi; |
| 16 | +// Types |
| 17 | +interface PasswordFormData { |
| 18 | + currentPassword: string; |
| 19 | + newPassword: string; |
| 20 | + confirmNewPassword: string; |
| 21 | +} |
21 | 22 |
|
22 | | - constructor(props: RouteComponentProps<any>) { |
23 | | - super(props); |
24 | | - this.state = {connecting: false}; |
25 | | - } |
| 23 | +// Password form validation |
| 24 | +const validatePasswordForm = (params: PasswordFormData) => ({ |
| 25 | + currentPassword: !params.currentPassword && 'Current password is required.', |
| 26 | + newPassword: (!params.newPassword && 'New password is required.') || (params.newPassword !== params.confirmNewPassword && 'Passwords do not match.'), |
| 27 | + confirmNewPassword: (!params.confirmNewPassword || params.confirmNewPassword !== params.newPassword) && 'Please confirm your new password.' |
| 28 | +}); |
26 | 29 |
|
27 | | - public render() { |
28 | | - return ( |
29 | | - <DataLoader key='userInfo' load={() => services.users.get()}> |
30 | | - {userInfo => ( |
31 | | - <Page |
32 | | - title='User Info' |
33 | | - toolbar={{ |
34 | | - breadcrumbs: [{title: 'User Info'}], |
35 | | - actionMenu: |
36 | | - userInfo.loggedIn && userInfo.iss === 'argocd' |
37 | | - ? { |
38 | | - items: [ |
39 | | - { |
40 | | - iconClassName: 'fa fa-lock', |
41 | | - title: 'Update Password', |
42 | | - action: () => (this.showChangePassword = true) |
43 | | - } |
44 | | - ] |
45 | | - } |
46 | | - : {items: []} |
47 | | - }}> |
48 | | - <div> |
49 | | - <div className='user-info'> |
50 | | - <div className='argo-container'> |
51 | | - <div className='user-info-overview__panel white-box'> |
52 | | - {userInfo.loggedIn ? ( |
53 | | - <React.Fragment key='userInfoInner'> |
54 | | - <p key='username'>Username: {userInfo.username}</p> |
55 | | - <p key='iss'>Issuer: {userInfo.iss}</p> |
56 | | - {userInfo.groups && ( |
57 | | - <React.Fragment key='userInfo4'> |
58 | | - <p>Groups:</p> |
59 | | - <ul> |
60 | | - {userInfo.groups.map(group => ( |
61 | | - <li key={group}>{group}</li> |
62 | | - ))} |
63 | | - </ul> |
64 | | - </React.Fragment> |
65 | | - )} |
66 | | - </React.Fragment> |
67 | | - ) : ( |
68 | | - <p key='loggedOutMessage'>You are not logged in</p> |
69 | | - )} |
70 | | - </div> |
71 | | - </div> |
72 | | - </div> |
73 | | - {userInfo.loggedIn && userInfo.iss === 'argocd' ? ( |
74 | | - <SlidingPanel |
75 | | - isShown={this.showChangePassword} |
76 | | - onClose={() => (this.showChangePassword = false)} |
77 | | - header={ |
78 | | - <div> |
79 | | - <button |
80 | | - className='argo-button argo-button--base' |
81 | | - onClick={() => { |
82 | | - this.formApiPassword.submitForm(null); |
83 | | - }}> |
84 | | - <Spinner show={this.state.connecting} style={{marginRight: '5px'}} /> |
85 | | - Save New Password |
86 | | - </button>{' '} |
87 | | - <button onClick={() => (this.showChangePassword = false)} className='argo-button argo-button--base-o'> |
88 | | - Cancel |
89 | | - </button> |
90 | | - </div> |
91 | | - }> |
92 | | - <h4>Update account password</h4> |
93 | | - <Form |
94 | | - onSubmit={params => this.changePassword(userInfo.username, params.currentPassword, params.newPassword)} |
95 | | - getApi={api => (this.formApiPassword = api)} |
96 | | - defaultValues={{type: 'git'}} |
97 | | - validateError={(params: {currentPassword: string; newPassword: string; confirmNewPassword: string}) => ({ |
98 | | - currentPassword: !params.currentPassword && 'Current password is required.', |
99 | | - newPassword: |
100 | | - (!params.newPassword && 'New password is required.') || |
101 | | - (params.newPassword !== params.confirmNewPassword && 'Confirm your new password.'), |
102 | | - confirmNewPassword: (!params.confirmNewPassword || params.confirmNewPassword !== params.newPassword) && 'Confirm your new password.' |
103 | | - })}> |
104 | | - {formApi => ( |
105 | | - <form onSubmit={formApi.submitForm} role='form' className='change-password width-control'> |
106 | | - <div className='argo-form-row'> |
107 | | - <FormField |
108 | | - formApi={formApi} |
109 | | - label='Current Password' |
110 | | - field='currentPassword' |
111 | | - component={Text} |
112 | | - componentProps={{type: 'password'}} |
113 | | - /> |
114 | | - </div> |
115 | | - <div className='argo-form-row'> |
116 | | - <FormField formApi={formApi} label='New Password' field='newPassword' component={Text} componentProps={{type: 'password'}} /> |
117 | | - </div> |
118 | | - <div className='argo-form-row'> |
119 | | - <FormField |
120 | | - formApi={formApi} |
121 | | - label='Confirm New Password' |
122 | | - field='confirmNewPassword' |
123 | | - component={Text} |
124 | | - componentProps={{type: 'password'}} |
125 | | - /> |
126 | | - </div> |
127 | | - </form> |
128 | | - )} |
129 | | - </Form> |
130 | | - </SlidingPanel> |
131 | | - ) : ( |
132 | | - <div /> |
133 | | - )} |
134 | | - </div> |
135 | | - </Page> |
136 | | - )} |
137 | | - </DataLoader> |
138 | | - ); |
139 | | - } |
| 30 | +export const UserInfoComponent = ({userInfo}: {userInfo: UserInfo}) => { |
| 31 | + const appContext = useContext(Context); |
| 32 | + |
| 33 | + const [isConnecting, setIsConnecting] = useState(false); |
| 34 | + const [showChangePassword, setShowChangePassword] = useState(new URLSearchParams(appContext.history.location.search).get(CHANGE_PASSWORD_PARAM) === 'true'); |
| 35 | + |
| 36 | + const formApiPassword = useRef<FormApi>(null); |
| 37 | + |
| 38 | + const changePassword = useCallback( |
| 39 | + async (username: string, currentPassword: Nested<FormValue> | FormValue, newPassword: Nested<FormValue> | FormValue) => { |
| 40 | + setIsConnecting(true); |
| 41 | + try { |
| 42 | + await services.accounts.changePassword(username, currentPassword, newPassword); |
| 43 | + appContext.notifications.show({ |
| 44 | + type: NotificationType.Success, |
| 45 | + content: 'Your password has been successfully updated.' |
| 46 | + }); |
| 47 | + setShowChangePassword(false); |
| 48 | + // Clear the URL parameter |
| 49 | + appContext.history.push(appContext.history.location.pathname); |
| 50 | + } catch (e) { |
| 51 | + appContext.notifications.show({ |
| 52 | + content: <ErrorNotification title='Unable to update your password.' e={e} />, |
| 53 | + type: NotificationType.Error |
| 54 | + }); |
| 55 | + } finally { |
| 56 | + setIsConnecting(false); |
| 57 | + } |
| 58 | + }, |
| 59 | + [appContext.notifications, appContext.history] |
| 60 | + ); |
140 | 61 |
|
141 | | - private async changePassword(username: string, currentPassword: Nested<FormValue> | FormValue, newPassword: Nested<FormValue> | FormValue) { |
142 | | - try { |
143 | | - await services.accounts.changePassword(username, currentPassword, newPassword); |
144 | | - this.appContext.apis.notifications.show({type: NotificationType.Success, content: 'Your password has been successfully updated.'}); |
145 | | - this.showChangePassword = false; |
146 | | - } catch (e) { |
147 | | - this.appContext.apis.notifications.show({ |
148 | | - content: <ErrorNotification title='Unable to update your password.' e={e} />, |
149 | | - type: NotificationType.Error |
150 | | - }); |
151 | | - } |
152 | | - } |
| 62 | + const clearChangePasswordForm = useCallback(() => { |
| 63 | + formApiPassword.current?.resetAll(); |
| 64 | + }, []); |
153 | 65 |
|
154 | | - // Whether to show the HTTPS repository connection dialogue on the page |
155 | | - private get showChangePassword() { |
156 | | - return new URLSearchParams(this.props.location.search).get('changePassword') === 'true'; |
157 | | - } |
| 66 | + const updateShowChangePassword = useCallback( |
| 67 | + (val: boolean) => { |
| 68 | + setShowChangePassword(val); |
| 69 | + clearChangePasswordForm(); |
| 70 | + const url = `${appContext.history.location.pathname}${val ? `?${CHANGE_PASSWORD_PARAM}=true` : ''}`; |
| 71 | + appContext.history.push(url); |
| 72 | + }, |
| 73 | + [appContext.history, clearChangePasswordForm] |
| 74 | + ); |
158 | 75 |
|
159 | | - private set showChangePassword(val: boolean) { |
160 | | - this.clearChangePasswordForm(); |
161 | | - this.appContext.router.history.push(`${this.props.match.url}?changePassword=${val}`); |
162 | | - } |
| 76 | + const handleFormSubmit = useCallback(() => { |
| 77 | + formApiPassword.current?.submitForm(null); |
| 78 | + }, []); |
163 | 79 |
|
164 | | - // Empty all fields in HTTPS repository form |
165 | | - private clearChangePasswordForm() { |
166 | | - this.formApiPassword.resetAll(); |
167 | | - } |
| 80 | + const isPasswordChangeAvailable = userInfo.loggedIn && userInfo.iss === ARGOCD_ISSUER; |
| 81 | + |
| 82 | + return ( |
| 83 | + <Page |
| 84 | + title='User Info' |
| 85 | + toolbar={{ |
| 86 | + breadcrumbs: [{title: 'User Info'}], |
| 87 | + actionMenu: isPasswordChangeAvailable |
| 88 | + ? { |
| 89 | + items: [ |
| 90 | + { |
| 91 | + iconClassName: 'fa fa-lock', |
| 92 | + title: 'Update Password', |
| 93 | + action: () => updateShowChangePassword(true) |
| 94 | + } |
| 95 | + ] |
| 96 | + } |
| 97 | + : {items: []} |
| 98 | + }}> |
| 99 | + <div> |
| 100 | + <div className='user-info'> |
| 101 | + <div className='argo-container'> |
| 102 | + <div className='user-info-overview__panel white-box'> |
| 103 | + {userInfo.loggedIn ? ( |
| 104 | + <> |
| 105 | + <p>Username: {userInfo.username}</p> |
| 106 | + <p>Issuer: {userInfo.iss}</p> |
| 107 | + {userInfo.groups && userInfo.groups.length > 0 && ( |
| 108 | + <> |
| 109 | + <p>Groups:</p> |
| 110 | + <ul> |
| 111 | + {userInfo.groups.map(group => ( |
| 112 | + <li key={group}>{group}</li> |
| 113 | + ))} |
| 114 | + </ul> |
| 115 | + </> |
| 116 | + )} |
| 117 | + </> |
| 118 | + ) : ( |
| 119 | + <p>You are not logged in</p> |
| 120 | + )} |
| 121 | + </div> |
| 122 | + </div> |
| 123 | + </div> |
| 124 | + {isPasswordChangeAvailable && ( |
| 125 | + <SlidingPanel |
| 126 | + isShown={showChangePassword} |
| 127 | + onClose={() => updateShowChangePassword(false)} |
| 128 | + header={ |
| 129 | + <div> |
| 130 | + <button className='argo-button argo-button--base' onClick={handleFormSubmit} disabled={isConnecting}> |
| 131 | + <Spinner show={isConnecting} style={{marginRight: '5px'}} /> |
| 132 | + Save New Password |
| 133 | + </button>{' '} |
| 134 | + <button onClick={() => updateShowChangePassword(false)} className='argo-button argo-button--base-o' disabled={isConnecting}> |
| 135 | + Cancel |
| 136 | + </button> |
| 137 | + </div> |
| 138 | + }> |
| 139 | + <h4>Update account password</h4> |
| 140 | + <Form |
| 141 | + onSubmit={(params: PasswordFormData) => changePassword(userInfo.username, params.currentPassword, params.newPassword)} |
| 142 | + getApi={api => (formApiPassword.current = api)} |
| 143 | + validateError={validatePasswordForm}> |
| 144 | + {formApi => ( |
| 145 | + <form onSubmit={formApi.submitForm} role='form' className='change-password width-control'> |
| 146 | + <div className='argo-form-row'> |
| 147 | + <FormField |
| 148 | + formApi={formApi} |
| 149 | + label='Current Password' |
| 150 | + field='currentPassword' |
| 151 | + component={Text} |
| 152 | + componentProps={{ |
| 153 | + type: 'password', |
| 154 | + autoComplete: 'current-password', |
| 155 | + disabled: isConnecting |
| 156 | + }} |
| 157 | + /> |
| 158 | + </div> |
| 159 | + <div className='argo-form-row'> |
| 160 | + <FormField |
| 161 | + formApi={formApi} |
| 162 | + label='New Password' |
| 163 | + field='newPassword' |
| 164 | + component={Text} |
| 165 | + componentProps={{ |
| 166 | + type: 'password', |
| 167 | + autoComplete: 'new-password', |
| 168 | + disabled: isConnecting |
| 169 | + }} |
| 170 | + /> |
| 171 | + </div> |
| 172 | + <div className='argo-form-row'> |
| 173 | + <FormField |
| 174 | + formApi={formApi} |
| 175 | + label='Confirm New Password' |
| 176 | + field='confirmNewPassword' |
| 177 | + component={Text} |
| 178 | + componentProps={{ |
| 179 | + type: 'password', |
| 180 | + autoComplete: 'new-password', |
| 181 | + disabled: isConnecting |
| 182 | + }} |
| 183 | + /> |
| 184 | + </div> |
| 185 | + </form> |
| 186 | + )} |
| 187 | + </Form> |
| 188 | + </SlidingPanel> |
| 189 | + )} |
| 190 | + </div> |
| 191 | + </Page> |
| 192 | + ); |
| 193 | +}; |
168 | 194 |
|
169 | | - private get appContext(): AppContext { |
170 | | - return this.context as AppContext; |
171 | | - } |
| 195 | +export function UserInfoOverview() { |
| 196 | + return <DataLoader load={() => services.users.get()}>{userInfo => <UserInfoComponent userInfo={userInfo} />}</DataLoader>; |
172 | 197 | } |
0 commit comments