mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
A11y/UserAdminPage: Improves tab navigation and focus management (#41321)
This commit is contained in:
@@ -8,19 +8,30 @@ interface Props {
|
||||
'aria-label'?: string;
|
||||
inputId?: string;
|
||||
onChange: (role: OrgRole) => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key }));
|
||||
|
||||
export const OrgRolePicker: FC<Props> = ({ value, onChange, 'aria-label': ariaLabel, inputId, ...restProps }) => (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
inputId={inputId}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(val) => onChange(val.value as OrgRole)}
|
||||
placeholder="Choose role..."
|
||||
aria-label={ariaLabel}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
export const OrgRolePicker: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
'aria-label': ariaLabel,
|
||||
inputId,
|
||||
autoFocus,
|
||||
...restProps
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
inputId={inputId}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(val) => onChange(val.value as OrgRole)}
|
||||
placeholder="Choose role..."
|
||||
aria-label={ariaLabel}
|
||||
autoFocus={autoFocus}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,12 +33,19 @@ interface State {
|
||||
}
|
||||
|
||||
export class UserOrgs extends PureComponent<Props, State> {
|
||||
addToOrgButtonRef = React.createRef<HTMLButtonElement>();
|
||||
state = {
|
||||
showAddOrgModal: false,
|
||||
};
|
||||
|
||||
showOrgAddModal = (show: boolean) => () => {
|
||||
this.setState({ showAddOrgModal: show });
|
||||
showOrgAddModal = () => {
|
||||
this.setState({ showAddOrgModal: true });
|
||||
};
|
||||
|
||||
dismissOrgAddModal = () => {
|
||||
this.setState({ showAddOrgModal: false }, () => {
|
||||
this.addToOrgButtonRef.current?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -69,12 +76,12 @@ export class UserOrgs extends PureComponent<Props, State> {
|
||||
</div>
|
||||
<div className={addToOrgContainerClass}>
|
||||
{canAddToOrg && (
|
||||
<Button variant="secondary" onClick={this.showOrgAddModal(true)}>
|
||||
<Button variant="secondary" onClick={this.showOrgAddModal} ref={this.addToOrgButtonRef}>
|
||||
Add user to organization
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
|
||||
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.dismissOrgAddModal} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -159,7 +166,7 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
||||
</td>
|
||||
{isChangingRole ? (
|
||||
<td>
|
||||
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} />
|
||||
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} autoFocus />
|
||||
</td>
|
||||
) : (
|
||||
<td className="width-25">{org.role}</td>
|
||||
@@ -184,6 +191,7 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
||||
confirmVariant="destructive"
|
||||
onCancel={this.onCancelClick}
|
||||
onConfirm={this.onOrgRemove}
|
||||
autoFocus
|
||||
>
|
||||
Remove from organization
|
||||
</ConfirmButton>
|
||||
@@ -260,7 +268,7 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
|
||||
onDismiss={this.onCancel}
|
||||
>
|
||||
<Field label="Organization">
|
||||
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} />
|
||||
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} autoFocus />
|
||||
</Field>
|
||||
<Field label="Role">
|
||||
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { ConfirmButton, RadioButtonGroup, Icon } from '@grafana/ui';
|
||||
import { cx } from '@emotion/css';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
@@ -10,98 +9,72 @@ interface Props {
|
||||
onGrafanaAdminChange: (isGrafanaAdmin: boolean) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isEditing: boolean;
|
||||
currentAdminOption: string;
|
||||
}
|
||||
|
||||
const adminOptions = [
|
||||
{ label: 'Yes', value: 'YES' },
|
||||
{ label: 'No', value: 'NO' },
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
export class UserPermissions extends PureComponent<Props, State> {
|
||||
state = {
|
||||
isEditing: false,
|
||||
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
||||
export function UserPermissions({ isGrafanaAdmin, onGrafanaAdminChange }: Props) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentAdminOption, setCurrentAdminOption] = useState(isGrafanaAdmin);
|
||||
|
||||
const onChangeClick = () => setIsEditing(true);
|
||||
|
||||
const onCancelClick = () => {
|
||||
setIsEditing(false);
|
||||
setCurrentAdminOption(isGrafanaAdmin);
|
||||
};
|
||||
|
||||
onChangeClick = () => {
|
||||
this.setState({ isEditing: true });
|
||||
};
|
||||
const handleGrafanaAdminChange = () => onGrafanaAdminChange(currentAdminOption);
|
||||
|
||||
onCancelClick = () => {
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
||||
});
|
||||
};
|
||||
const canChangePermissions = contextSrv.hasPermission(AccessControlAction.UsersPermissionsUpdate);
|
||||
|
||||
onGrafanaAdminChange = () => {
|
||||
const { currentAdminOption } = this.state;
|
||||
const newIsGrafanaAdmin = currentAdminOption === 'YES' ? true : false;
|
||||
this.props.onGrafanaAdminChange(newIsGrafanaAdmin);
|
||||
};
|
||||
|
||||
onAdminOptionSelect = (value: string) => {
|
||||
this.setState({ currentAdminOption: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isGrafanaAdmin } = this.props;
|
||||
const { isEditing, currentAdminOption } = this.state;
|
||||
const changeButtonContainerClass = cx('pull-right');
|
||||
const canChangePermissions = contextSrv.hasPermission(AccessControlAction.UsersPermissionsUpdate);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">Permissions</h3>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="width-16">Grafana Admin</td>
|
||||
{isEditing ? (
|
||||
<td colSpan={2}>
|
||||
<RadioButtonGroup
|
||||
options={adminOptions}
|
||||
value={currentAdminOption}
|
||||
onChange={this.onAdminOptionSelect}
|
||||
/>
|
||||
</td>
|
||||
) : (
|
||||
<td colSpan={2}>
|
||||
{isGrafanaAdmin ? (
|
||||
<>
|
||||
<Icon name="shield" /> Yes
|
||||
</>
|
||||
) : (
|
||||
<>No</>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<div className={changeButtonContainerClass}>
|
||||
{canChangePermissions && (
|
||||
<ConfirmButton
|
||||
className="pull-right"
|
||||
onClick={this.onChangeClick}
|
||||
onConfirm={this.onGrafanaAdminChange}
|
||||
onCancel={this.onCancelClick}
|
||||
confirmText="Change"
|
||||
>
|
||||
Change
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">Permissions</h3>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="width-16">Grafana Admin</td>
|
||||
{isEditing ? (
|
||||
<td colSpan={2}>
|
||||
<RadioButtonGroup
|
||||
options={adminOptions}
|
||||
value={currentAdminOption}
|
||||
onChange={setCurrentAdminOption}
|
||||
autoFocus
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<td colSpan={2}>
|
||||
{isGrafanaAdmin ? (
|
||||
<>
|
||||
<Icon name="shield" /> Yes
|
||||
</>
|
||||
) : (
|
||||
<>No</>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
{canChangePermissions && (
|
||||
<ConfirmButton
|
||||
onClick={onChangeClick}
|
||||
onConfirm={handleGrafanaAdminChange}
|
||||
onCancel={onCancelClick}
|
||||
confirmText="Change"
|
||||
>
|
||||
Change
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, PureComponent } from 'react';
|
||||
import React, { FC, PureComponent, useRef, useState } from 'react';
|
||||
import { AccessControlAction, UserDTO } from 'app/types';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { config } from 'app/core/config';
|
||||
@@ -16,163 +16,149 @@ interface Props {
|
||||
onPasswordChange(password: string): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
showDeleteModal: boolean;
|
||||
showDisableModal: boolean;
|
||||
}
|
||||
export function UserProfile({
|
||||
user,
|
||||
onUserUpdate,
|
||||
onUserDelete,
|
||||
onUserDisable,
|
||||
onUserEnable,
|
||||
onPasswordChange,
|
||||
}: Props) {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showDisableModal, setShowDisableModal] = useState(false);
|
||||
|
||||
export class UserProfile extends PureComponent<Props, State> {
|
||||
state = {
|
||||
isLoading: false,
|
||||
showDeleteModal: false,
|
||||
showDisableModal: false,
|
||||
const deleteUserRef = useRef<HTMLButtonElement | null>(null);
|
||||
const showDeleteUserModal = (show: boolean) => () => {
|
||||
setShowDeleteModal(show);
|
||||
if (!show && deleteUserRef.current) {
|
||||
deleteUserRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
showDeleteUserModal = (show: boolean) => () => {
|
||||
this.setState({ showDeleteModal: show });
|
||||
const disableUserRef = useRef<HTMLButtonElement | null>(null);
|
||||
const showDisableUserModal = (show: boolean) => () => {
|
||||
setShowDisableModal(show);
|
||||
if (!show && disableUserRef.current) {
|
||||
disableUserRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
showDisableUserModal = (show: boolean) => () => {
|
||||
this.setState({ showDisableModal: show });
|
||||
};
|
||||
const handleUserDelete = () => onUserDelete(user.id);
|
||||
|
||||
onUserDelete = () => {
|
||||
const { user, onUserDelete } = this.props;
|
||||
onUserDelete(user.id);
|
||||
};
|
||||
const handleUserDisable = () => onUserDisable(user.id);
|
||||
|
||||
onUserDisable = () => {
|
||||
const { user, onUserDisable } = this.props;
|
||||
onUserDisable(user.id);
|
||||
};
|
||||
const handleUserEnable = () => onUserEnable(user.id);
|
||||
|
||||
onUserEnable = () => {
|
||||
const { user, onUserEnable } = this.props;
|
||||
onUserEnable(user.id);
|
||||
};
|
||||
|
||||
onUserNameChange = (newValue: string) => {
|
||||
const { user, onUserUpdate } = this.props;
|
||||
const onUserNameChange = (newValue: string) => {
|
||||
onUserUpdate({
|
||||
...user,
|
||||
name: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
onUserEmailChange = (newValue: string) => {
|
||||
const { user, onUserUpdate } = this.props;
|
||||
const onUserEmailChange = (newValue: string) => {
|
||||
onUserUpdate({
|
||||
...user,
|
||||
email: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
onUserLoginChange = (newValue: string) => {
|
||||
const { user, onUserUpdate } = this.props;
|
||||
const onUserLoginChange = (newValue: string) => {
|
||||
onUserUpdate({
|
||||
...user,
|
||||
login: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
onPasswordChange = (newValue: string) => {
|
||||
this.props.onPasswordChange(newValue);
|
||||
};
|
||||
const authSource = user.authLabels?.length && user.authLabels[0];
|
||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||
const styles = getStyles(config.theme);
|
||||
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { showDeleteModal, showDisableModal } = this.state;
|
||||
const authSource = user.authLabels?.length && user.authLabels[0];
|
||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||
const styles = getStyles(config.theme);
|
||||
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
||||
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
||||
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
||||
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
||||
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
||||
|
||||
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
||||
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
||||
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
||||
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
||||
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
||||
|
||||
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={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={this.onUserNameChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Email"
|
||||
value={user.email}
|
||||
locked={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={this.onUserEmailChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Username"
|
||||
value={user.login}
|
||||
locked={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={this.onUserLoginChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Password"
|
||||
value="********"
|
||||
inputType="password"
|
||||
locked={passwordChangeLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={this.onPasswordChange}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.buttonRow}>
|
||||
{canDelete && (
|
||||
<>
|
||||
<Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
|
||||
Delete user
|
||||
</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 && canEnable && (
|
||||
<Button variant="secondary" onClick={this.onUserEnable}>
|
||||
Enable user
|
||||
</Button>
|
||||
)}
|
||||
{!user.isDisabled && canDisable && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={this.showDisableUserModal(true)}>
|
||||
Disable user
|
||||
</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>
|
||||
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={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={onUserNameChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Email"
|
||||
value={user.email}
|
||||
locked={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={onUserEmailChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Username"
|
||||
value={user.login}
|
||||
locked={editLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={onUserLoginChange}
|
||||
/>
|
||||
<UserProfileRow
|
||||
label="Password"
|
||||
value="********"
|
||||
inputType="password"
|
||||
locked={passwordChangeLocked}
|
||||
lockMessage={lockMessage}
|
||||
onChange={onPasswordChange}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<div className={styles.buttonRow}>
|
||||
{canDelete && (
|
||||
<>
|
||||
<Button variant="destructive" onClick={showDeleteUserModal(true)} ref={deleteUserRef}>
|
||||
Delete user
|
||||
</Button>
|
||||
<ConfirmModal
|
||||
isOpen={showDeleteModal}
|
||||
title="Delete user"
|
||||
body="Are you sure you want to delete this user?"
|
||||
confirmText="Delete user"
|
||||
onConfirm={handleUserDelete}
|
||||
onDismiss={showDeleteUserModal(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{user.isDisabled && canEnable && (
|
||||
<Button variant="secondary" onClick={handleUserEnable}>
|
||||
Enable user
|
||||
</Button>
|
||||
)}
|
||||
{!user.isDisabled && canDisable && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={showDisableUserModal(true)} ref={disableUserRef}>
|
||||
Disable user
|
||||
</Button>
|
||||
<ConfirmModal
|
||||
isOpen={showDisableModal}
|
||||
title="Disable user"
|
||||
body="Are you sure you want to disable this user?"
|
||||
confirmText="Disable user"
|
||||
onConfirm={handleUserDisable}
|
||||
onDismiss={showDisableUserModal(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
@@ -269,7 +255,6 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
|
||||
font-weight: 500;
|
||||
`
|
||||
);
|
||||
const editButtonContainerClass = cx('pull-right');
|
||||
|
||||
if (locked) {
|
||||
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
||||
@@ -297,16 +282,14 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className={editButtonContainerClass}>
|
||||
<ConfirmButton
|
||||
confirmText="Save"
|
||||
onClick={this.onEditClick}
|
||||
onConfirm={this.onSave}
|
||||
onCancel={this.onCancelClick}
|
||||
>
|
||||
Edit
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
<ConfirmButton
|
||||
confirmText="Save"
|
||||
onClick={this.onEditClick}
|
||||
onConfirm={this.onSave}
|
||||
onCancel={this.onCancelClick}
|
||||
>
|
||||
Edit
|
||||
</ConfirmButton>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -320,13 +303,10 @@ interface LockedRowProps {
|
||||
}
|
||||
|
||||
export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
|
||||
const lockMessageClass = cx(
|
||||
'pull-right',
|
||||
css`
|
||||
font-style: italic;
|
||||
margin-right: 0.6rem;
|
||||
`
|
||||
);
|
||||
const lockMessageClass = css`
|
||||
font-style: italic;
|
||||
margin-right: 0.6rem;
|
||||
`;
|
||||
const labelClass = cx(
|
||||
'width-16',
|
||||
css`
|
||||
|
||||
@@ -16,12 +16,19 @@ interface State {
|
||||
}
|
||||
|
||||
export class UserSessions extends PureComponent<Props, State> {
|
||||
forceAllLogoutButton = React.createRef<HTMLButtonElement>();
|
||||
state: State = {
|
||||
showLogoutModal: false,
|
||||
};
|
||||
|
||||
showLogoutConfirmationModal = (show: boolean) => () => {
|
||||
this.setState({ showLogoutModal: show });
|
||||
showLogoutConfirmationModal = () => {
|
||||
this.setState({ showLogoutModal: true });
|
||||
};
|
||||
|
||||
dismissLogoutConfirmationModal = () => {
|
||||
this.setState({ showLogoutModal: false }, () => {
|
||||
this.forceAllLogoutButton.current?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
onSessionRevoke = (id: number) => {
|
||||
@@ -74,6 +81,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
||||
confirmText="Confirm logout"
|
||||
confirmVariant="destructive"
|
||||
onConfirm={this.onSessionRevoke(session.id)}
|
||||
autoFocus
|
||||
>
|
||||
Force logout
|
||||
</ConfirmButton>
|
||||
@@ -87,7 +95,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
||||
</div>
|
||||
<div className={logoutFromAllDevicesClass}>
|
||||
{canLogout && sessions.length > 0 && (
|
||||
<Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
|
||||
<Button variant="secondary" onClick={this.showLogoutConfirmationModal} ref={this.forceAllLogoutButton}>
|
||||
Force logout from all devices
|
||||
</Button>
|
||||
)}
|
||||
@@ -97,7 +105,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
||||
body="Are you sure you want to force logout from all devices?"
|
||||
confirmText="Force logout"
|
||||
onConfirm={this.onAllSessionsRevoke}
|
||||
onDismiss={this.showLogoutConfirmationModal(false)}
|
||||
onDismiss={this.dismissLogoutConfirmationModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,9 +75,9 @@ describe('ApiKeysPage', () => {
|
||||
setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true });
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('row').length).toBe(4);
|
||||
expect(screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00 cancel delete/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00 cancel delete/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /third viewer no expiration date cancel delete/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /third viewer no expiration date/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,27 +118,24 @@ describe('ApiKeysPage', () => {
|
||||
{ id: 3, name: 'Third', role: OrgRole.Viewer, secondsToLive: 0, expiration: undefined },
|
||||
];
|
||||
const { deleteApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true });
|
||||
const firstRow = screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00 cancel delete/i });
|
||||
const secondRow = screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00 cancel delete/i });
|
||||
const firstRow = screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00/i });
|
||||
const secondRow = screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00/i });
|
||||
|
||||
deleteApiKeyMock.mockClear();
|
||||
expect(within(firstRow).getByRole('cell', { name: /cancel delete/i })).toBeInTheDocument();
|
||||
userEvent.click(within(firstRow).getByRole('cell', { name: /cancel delete/i }));
|
||||
expect(within(firstRow).getByLabelText('Delete API key')).toBeInTheDocument();
|
||||
userEvent.click(within(firstRow).getByLabelText('Delete API key'));
|
||||
|
||||
expect(within(firstRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument();
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
userEvent.click(within(firstRow).getByRole('button', { name: /delete$/i }), undefined, {
|
||||
skipPointerEventsCheck: true,
|
||||
});
|
||||
userEvent.click(within(firstRow).getByRole('button', { name: /delete$/i }));
|
||||
expect(deleteApiKeyMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteApiKeyMock).toHaveBeenCalledWith(1, false);
|
||||
|
||||
toggleShowExpired();
|
||||
|
||||
deleteApiKeyMock.mockClear();
|
||||
expect(within(secondRow).getByRole('cell', { name: /cancel delete/i })).toBeInTheDocument();
|
||||
userEvent.click(within(secondRow).getByRole('cell', { name: /cancel delete/i }));
|
||||
expect(within(secondRow).getByLabelText('Delete API key')).toBeInTheDocument();
|
||||
userEvent.click(within(secondRow).getByLabelText('Delete API key'));
|
||||
expect(within(secondRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument();
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
userEvent.click(within(secondRow).getByRole('button', { name: /delete$/i }), undefined, {
|
||||
skipPointerEventsCheck: true,
|
||||
});
|
||||
|
||||
@@ -116,9 +116,7 @@ describe('AnnotationsSettings', () => {
|
||||
|
||||
expect(screen.getByRole('heading', { name: /annotations/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('table')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana cancel delete/i })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
||||
).toBeInTheDocument();
|
||||
@@ -142,14 +140,14 @@ describe('AnnotationsSettings', () => {
|
||||
userEvent.click(within(heading).getByText(/annotations/i));
|
||||
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /my annotation \(built\-in\) grafana cancel delete/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: /my annotation \(built\-in\) grafana/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /new query/i })).not.toBeInTheDocument();
|
||||
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
userEvent.click(screen.getByRole('button', { name: /^delete$/i }), undefined, { skipPointerEventsCheck: true });
|
||||
userEvent.click(screen.getAllByLabelText(/Delete query with title/)[0]);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
expect(screen.queryAllByRole('row').length).toBe(0);
|
||||
expect(
|
||||
@@ -238,9 +236,7 @@ describe('AnnotationsSettings', () => {
|
||||
userEvent.click(within(heading).getByText(/annotations/i));
|
||||
|
||||
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
||||
expect(
|
||||
screen.queryByRole('row', { name: /my prometheus annotation prometheus cancel delete/i })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole('row', { name: /my prometheus annotation prometheus/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /new query/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
||||
@@ -252,8 +248,8 @@ describe('AnnotationsSettings', () => {
|
||||
|
||||
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(3);
|
||||
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
userEvent.click(screen.getAllByRole('button', { name: /delete/i })[1], undefined, { skipPointerEventsCheck: true });
|
||||
userEvent.click(screen.getAllByLabelText(/Delete query with title/)[0]);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
||||
});
|
||||
|
||||
@@ -131,10 +131,8 @@ describe('LinksSettings', () => {
|
||||
|
||||
expect(getTableBodyRows().length).toBe(links.length);
|
||||
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
userEvent.click(within(getTableBody()).getAllByRole('button', { name: 'Delete' })[0], undefined, {
|
||||
skipPointerEventsCheck: true,
|
||||
});
|
||||
userEvent.click(within(getTableBody()).getAllByLabelText(/Delete link with title/)[0]);
|
||||
userEvent.click(within(getTableBody()).getByRole('button', { name: 'Delete' }));
|
||||
|
||||
expect(getTableBodyRows().length).toBe(links.length - 1);
|
||||
expect(within(getTableBody()).queryByText(links[0].title)).not.toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user