grafana/public/app/features/admin/UserProfile.tsx
Alexander Zobnin 8505d90768 Admin: New Admin User page (#20498)
* admin: user page to react WIP

* admin user page: basic view

* admin user page: refactor, extract orgs and permissions components

* admin user: change sessions actions styles

* admin user: add disable button

* user admin: add change grafana admin action

* user admin: able to change org role and remove org

* user admin: confirm force logout

* user admin: change org button style

* user admin: add confirm modals for critical actions

* user admin: lock down ldap user info

* user admin: align with latest design changes

* user admin: add LDAP sync

* admin user: confirm button

* user admin: add to org modal

* user admin: fix ConfirmButton story

* admin user: handle grafana admin change

* ConfirmButton: make styled component

* ConfirmButton: completely styled component

* User Admin: permissions section refactor

* admin user: refactor (orgs and sessions)

* ConfirmButton: able to set confirm variant

* admin user: inline org removal

* admin user: show ldap sync info only for ldap users

* admin user: edit profile

* ConfirmButton: some fixes after review

* Chore: fix storybook build

* admin user: rename handlers

* admin user: remove LdapUserPage import from routes

* Chore: fix ConfirmButton tests

* Chore: fix user api endpoint tests

* Chore: update failed test snapshots

* admin user: redux actions WIP

* admin user: use new ConfirmModal component for user profile

* admin user: use new ConfirmModal component for sessions

* admin user: use lockMessage

* ConfirmButton: use primary button as default

* admin user: fix ActionButton color

* UI: use Icon component for Modal

* UI: refactor ConfirmModal after Modal changes

* UI: add link button variant

* UI: able to use custom ConfirmButton

* Chore: fix type errors after ConfirmButton refactor

* Chore: revert Graph component changes (works with TS 3.7)

* Chore: use Forms.Button instead of ActionButton

* admin user: align items

* admin user: align add to org modal

* UI: organization picker component

* admin user: use org picker for AddToOrgModal

* admin user: org actions

* admin user: connect sessions actions

* admin user: updateUserPermissions action

* admin user: enable delete user action

* admin user: sync ldap user

* Chore: refactor, remove unused code

* Chore: refactor, move api calls to actions

* admin user: set user password action

* Chore: refactor, remove unused components

* admin user: set input focus on edit

* admin user: pass user into debug LDAP mapping

* UserAdminPage: Ux changes

* UserAdminPage: align buttons to the left

* UserAdminPage: align delete user button

* UserAdminPage: swap add to org modal buttons

* UserAdminPage: set password field to empty when editing

* UserAdminPage: fix tests

* Updated button border

* Chore: fix ConfirmButton after changes introduced in #21092

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
2020-01-13 17:10:19 +01:00

330 lines
8.2 KiB
TypeScript

import React, { PureComponent, FC } from 'react';
import { UserDTO } from 'app/types';
import { cx, css } from 'emotion';
import { config } from 'app/core/config';
import { GrafanaTheme } from '@grafana/data';
import { ConfirmButton, Input, ConfirmModal, InputStatus, Forms, stylesFactory } from '@grafana/ui';
interface Props {
user: UserDTO;
onUserUpdate: (user: UserDTO) => void;
onUserDelete: (userId: number) => void;
onUserDisable: (userId: number) => void;
onUserEnable: (userId: number) => void;
onPasswordChange(password: string): void;
}
interface State {
isLoading: boolean;
showDeleteModal: boolean;
showDisableModal: boolean;
}
export class UserProfile extends PureComponent<Props, State> {
state = {
isLoading: false,
showDeleteModal: false,
showDisableModal: false,
};
showDeleteUserModal = (show: boolean) => () => {
this.setState({ showDeleteModal: show });
};
showDisableUserModal = (show: boolean) => () => {
this.setState({ showDisableModal: show });
};
onUserDelete = () => {
const { user, onUserDelete } = this.props;
onUserDelete(user.id);
};
onUserDisable = () => {
const { user, onUserDisable } = this.props;
onUserDisable(user.id);
};
onUserEnable = () => {
const { user, onUserEnable } = this.props;
onUserEnable(user.id);
};
onUserNameChange = (newValue: string) => {
const { user, onUserUpdate } = this.props;
onUserUpdate({
...user,
name: newValue,
});
};
onUserEmailChange = (newValue: string) => {
const { user, onUserUpdate } = this.props;
onUserUpdate({
...user,
email: newValue,
});
};
onUserLoginChange = (newValue: string) => {
const { user, onUserUpdate } = this.props;
onUserUpdate({
...user,
login: newValue,
});
};
onPasswordChange = (newValue: string) => {
this.props.onPasswordChange(newValue);
};
render() {
const { user } = this.props;
const { showDeleteModal, showDisableModal } = this.state;
const lockMessage = 'Synced via LDAP';
const styles = getStyles(config.theme);
return (
<>
<h3 className="page-heading">User information</h3>
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<tbody>
<UserProfileRow
label="Name"
value={user.name}
locked={user.isExternal}
lockMessage={lockMessage}
onChange={this.onUserNameChange}
/>
<UserProfileRow
label="Email"
value={user.email}
locked={user.isExternal}
lockMessage={lockMessage}
onChange={this.onUserEmailChange}
/>
<UserProfileRow
label="Username"
value={user.login}
locked={user.isExternal}
lockMessage={lockMessage}
onChange={this.onUserLoginChange}
/>
<UserProfileRow
label="Password"
value="********"
inputType="password"
locked={user.isExternal}
lockMessage={lockMessage}
onChange={this.onPasswordChange}
/>
</tbody>
</table>
</div>
<div className={styles.buttonRow}>
<Forms.Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
Delete User
</Forms.Button>
<ConfirmModal
isOpen={showDeleteModal}
title="Delete user"
body="Are you sure you want to delete this user?"
confirmText="Delete user"
onConfirm={this.onUserDelete}
onDismiss={this.showDeleteUserModal(false)}
/>
{user.isDisabled ? (
<Forms.Button variant="secondary" onClick={this.onUserEnable}>
Enable User
</Forms.Button>
) : (
<Forms.Button variant="secondary" onClick={this.showDisableUserModal(true)}>
Disable User
</Forms.Button>
)}
<ConfirmModal
isOpen={showDisableModal}
title="Disable user"
body="Are you sure you want to disable this user?"
confirmText="Disable user"
onConfirm={this.onUserDisable}
onDismiss={this.showDisableUserModal(false)}
/>
</div>
</div>
</>
);
}
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
buttonRow: css`
margin-top: 0.8rem;
> * {
margin-right: 16px;
}
`,
};
});
interface UserProfileRowProps {
label: string;
value?: string;
locked?: boolean;
lockMessage?: string;
inputType?: string;
onChange?: (value: string) => void;
}
interface UserProfileRowState {
value: string;
editing: boolean;
}
export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfileRowState> {
inputElem: HTMLInputElement;
static defaultProps: Partial<UserProfileRowProps> = {
value: '',
locked: false,
lockMessage: '',
inputType: 'text',
};
state = {
editing: false,
value: this.props.value || '',
};
setInputElem = (elem: any) => {
this.inputElem = elem;
};
onEditClick = () => {
if (this.props.inputType === 'password') {
// Reset value for password field
this.setState({ editing: true, value: '' }, this.focusInput);
} else {
this.setState({ editing: true }, this.focusInput);
}
};
onCancelClick = () => {
this.setState({ editing: false, value: this.props.value || '' });
};
onInputChange = (event: React.ChangeEvent<HTMLInputElement>, status?: InputStatus) => {
if (status === InputStatus.Invalid) {
return;
}
this.setState({ value: event.target.value });
};
onInputBlur = (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => {
if (status === InputStatus.Invalid) {
return;
}
this.setState({ value: event.target.value });
};
focusInput = () => {
if (this.inputElem && this.inputElem.focus) {
this.inputElem.focus();
}
};
onSave = () => {
if (this.props.onChange) {
this.props.onChange(this.state.value);
}
};
render() {
const { label, locked, lockMessage, inputType } = this.props;
const { value } = this.state;
const labelClass = cx(
'width-16',
css`
font-weight: 500;
`
);
const editButtonContainerClass = cx('pull-right');
if (locked) {
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
}
return (
<tr>
<td className={labelClass}>{label}</td>
<td className="width-25" colSpan={2}>
{this.state.editing ? (
<Input
className="width-20"
type={inputType}
defaultValue={value}
onBlur={this.onInputBlur}
onChange={this.onInputChange}
inputRef={this.setInputElem}
/>
) : (
<span>{this.props.value}</span>
)}
</td>
<td>
<div className={editButtonContainerClass}>
<ConfirmButton
confirmText="Save"
onClick={this.onEditClick}
onConfirm={this.onSave}
onCancel={this.onCancelClick}
>
Edit
</ConfirmButton>
</div>
</td>
</tr>
);
}
}
interface LockedRowProps {
label: string;
value?: any;
lockMessage?: string;
}
export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
const lockMessageClass = cx(
'pull-right',
css`
font-style: italic;
margin-right: 0.6rem;
`
);
const labelClass = cx(
'width-16',
css`
font-weight: 500;
`
);
return (
<tr>
<td className={labelClass}>{label}</td>
<td className="width-25" colSpan={2}>
{value}
</td>
<td>
<span className={lockMessageClass}>{lockMessage}</span>
</td>
</tr>
);
};