UserProfile.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { css, cx } from '@emotion/css';
  2. import React, { FC, PureComponent, useRef, useState } from 'react';
  3. import { GrafanaTheme } from '@grafana/data';
  4. import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui';
  5. import { config } from 'app/core/config';
  6. import { contextSrv } from 'app/core/core';
  7. import { AccessControlAction, UserDTO } from 'app/types';
  8. interface Props {
  9. user: UserDTO;
  10. onUserUpdate: (user: UserDTO) => void;
  11. onUserDelete: (userId: number) => void;
  12. onUserDisable: (userId: number) => void;
  13. onUserEnable: (userId: number) => void;
  14. onPasswordChange(password: string): void;
  15. }
  16. export function UserProfile({
  17. user,
  18. onUserUpdate,
  19. onUserDelete,
  20. onUserDisable,
  21. onUserEnable,
  22. onPasswordChange,
  23. }: Props) {
  24. const [showDeleteModal, setShowDeleteModal] = useState(false);
  25. const [showDisableModal, setShowDisableModal] = useState(false);
  26. const deleteUserRef = useRef<HTMLButtonElement | null>(null);
  27. const showDeleteUserModal = (show: boolean) => () => {
  28. setShowDeleteModal(show);
  29. if (!show && deleteUserRef.current) {
  30. deleteUserRef.current.focus();
  31. }
  32. };
  33. const disableUserRef = useRef<HTMLButtonElement | null>(null);
  34. const showDisableUserModal = (show: boolean) => () => {
  35. setShowDisableModal(show);
  36. if (!show && disableUserRef.current) {
  37. disableUserRef.current.focus();
  38. }
  39. };
  40. const handleUserDelete = () => onUserDelete(user.id);
  41. const handleUserDisable = () => onUserDisable(user.id);
  42. const handleUserEnable = () => onUserEnable(user.id);
  43. const onUserNameChange = (newValue: string) => {
  44. onUserUpdate({
  45. ...user,
  46. name: newValue,
  47. });
  48. };
  49. const onUserEmailChange = (newValue: string) => {
  50. onUserUpdate({
  51. ...user,
  52. email: newValue,
  53. });
  54. };
  55. const onUserLoginChange = (newValue: string) => {
  56. onUserUpdate({
  57. ...user,
  58. login: newValue,
  59. });
  60. };
  61. const authSource = user.authLabels?.length && user.authLabels[0];
  62. const lockMessage = authSource ? `Synced via ${authSource}` : '';
  63. const styles = getStyles(config.theme);
  64. const editLocked = user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersWrite, user);
  65. const passwordChangeLocked =
  66. user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersPasswordUpdate, user);
  67. const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDelete, user);
  68. const canDisable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDisable, user);
  69. const canEnable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersEnable, user);
  70. return (
  71. <>
  72. <h3 className="page-heading">User information</h3>
  73. <div className="gf-form-group">
  74. <div className="gf-form">
  75. <table className="filter-table form-inline">
  76. <tbody>
  77. <UserProfileRow
  78. label="Name"
  79. value={user.name}
  80. locked={editLocked}
  81. lockMessage={lockMessage}
  82. onChange={onUserNameChange}
  83. />
  84. <UserProfileRow
  85. label="Email"
  86. value={user.email}
  87. locked={editLocked}
  88. lockMessage={lockMessage}
  89. onChange={onUserEmailChange}
  90. />
  91. <UserProfileRow
  92. label="Username"
  93. value={user.login}
  94. locked={editLocked}
  95. lockMessage={lockMessage}
  96. onChange={onUserLoginChange}
  97. />
  98. <UserProfileRow
  99. label="Password"
  100. value="********"
  101. inputType="password"
  102. locked={passwordChangeLocked}
  103. lockMessage={lockMessage}
  104. onChange={onPasswordChange}
  105. />
  106. </tbody>
  107. </table>
  108. </div>
  109. <div className={styles.buttonRow}>
  110. {canDelete && (
  111. <>
  112. <Button variant="destructive" onClick={showDeleteUserModal(true)} ref={deleteUserRef}>
  113. Delete user
  114. </Button>
  115. <ConfirmModal
  116. isOpen={showDeleteModal}
  117. title="Delete user"
  118. body="Are you sure you want to delete this user?"
  119. confirmText="Delete user"
  120. onConfirm={handleUserDelete}
  121. onDismiss={showDeleteUserModal(false)}
  122. />
  123. </>
  124. )}
  125. {user.isDisabled && canEnable && (
  126. <Button variant="secondary" onClick={handleUserEnable}>
  127. Enable user
  128. </Button>
  129. )}
  130. {!user.isDisabled && canDisable && (
  131. <>
  132. <Button variant="secondary" onClick={showDisableUserModal(true)} ref={disableUserRef}>
  133. Disable user
  134. </Button>
  135. <ConfirmModal
  136. isOpen={showDisableModal}
  137. title="Disable user"
  138. body="Are you sure you want to disable this user?"
  139. confirmText="Disable user"
  140. onConfirm={handleUserDisable}
  141. onDismiss={showDisableUserModal(false)}
  142. />
  143. </>
  144. )}
  145. </div>
  146. </div>
  147. </>
  148. );
  149. }
  150. const getStyles = stylesFactory((theme: GrafanaTheme) => {
  151. return {
  152. buttonRow: css`
  153. margin-top: 0.8rem;
  154. > * {
  155. margin-right: 16px;
  156. }
  157. `,
  158. };
  159. });
  160. interface UserProfileRowProps {
  161. label: string;
  162. value?: string;
  163. locked?: boolean;
  164. lockMessage?: string;
  165. inputType?: string;
  166. onChange?: (value: string) => void;
  167. }
  168. interface UserProfileRowState {
  169. value: string;
  170. editing: boolean;
  171. }
  172. export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfileRowState> {
  173. inputElem?: HTMLInputElement;
  174. static defaultProps: Partial<UserProfileRowProps> = {
  175. value: '',
  176. locked: false,
  177. lockMessage: '',
  178. inputType: 'text',
  179. };
  180. state = {
  181. editing: false,
  182. value: this.props.value || '',
  183. };
  184. setInputElem = (elem: any) => {
  185. this.inputElem = elem;
  186. };
  187. onEditClick = () => {
  188. if (this.props.inputType === 'password') {
  189. // Reset value for password field
  190. this.setState({ editing: true, value: '' }, this.focusInput);
  191. } else {
  192. this.setState({ editing: true }, this.focusInput);
  193. }
  194. };
  195. onCancelClick = () => {
  196. this.setState({ editing: false, value: this.props.value || '' });
  197. };
  198. onInputChange = (event: React.ChangeEvent<HTMLInputElement>, status?: LegacyInputStatus) => {
  199. if (status === LegacyInputStatus.Invalid) {
  200. return;
  201. }
  202. this.setState({ value: event.target.value });
  203. };
  204. onInputBlur = (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => {
  205. if (status === LegacyInputStatus.Invalid) {
  206. return;
  207. }
  208. this.setState({ value: event.target.value });
  209. };
  210. focusInput = () => {
  211. if (this.inputElem && this.inputElem.focus) {
  212. this.inputElem.focus();
  213. }
  214. };
  215. onSave = () => {
  216. if (this.props.onChange) {
  217. this.props.onChange(this.state.value);
  218. }
  219. };
  220. render() {
  221. const { label, locked, lockMessage, inputType } = this.props;
  222. const { value } = this.state;
  223. const labelClass = cx(
  224. 'width-16',
  225. css`
  226. font-weight: 500;
  227. `
  228. );
  229. if (locked) {
  230. return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
  231. }
  232. const inputId = `${label}-input`;
  233. return (
  234. <tr>
  235. <td className={labelClass}>
  236. <label htmlFor={inputId}>{label}</label>
  237. </td>
  238. <td className="width-25" colSpan={2}>
  239. {this.state.editing ? (
  240. <Input
  241. id={inputId}
  242. type={inputType}
  243. defaultValue={value}
  244. onBlur={this.onInputBlur}
  245. onChange={this.onInputChange}
  246. ref={this.setInputElem}
  247. width={30}
  248. />
  249. ) : (
  250. <span>{this.props.value}</span>
  251. )}
  252. </td>
  253. <td>
  254. <ConfirmButton
  255. confirmText="Save"
  256. onClick={this.onEditClick}
  257. onConfirm={this.onSave}
  258. onCancel={this.onCancelClick}
  259. >
  260. Edit
  261. </ConfirmButton>
  262. </td>
  263. </tr>
  264. );
  265. }
  266. }
  267. interface LockedRowProps {
  268. label: string;
  269. value?: any;
  270. lockMessage?: string;
  271. }
  272. export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
  273. const lockMessageClass = css`
  274. font-style: italic;
  275. margin-right: 0.6rem;
  276. `;
  277. const labelClass = cx(
  278. 'width-16',
  279. css`
  280. font-weight: 500;
  281. `
  282. );
  283. return (
  284. <tr>
  285. <td className={labelClass}>{label}</td>
  286. <td className="width-25" colSpan={2}>
  287. {value}
  288. </td>
  289. <td>
  290. <span className={lockMessageClass}>{lockMessage}</span>
  291. </td>
  292. </tr>
  293. );
  294. };