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:
parent
f3f57d3d6e
commit
c8b7373016
@ -21,6 +21,8 @@ export interface Props extends Themeable {
|
|||||||
confirmVariant?: ButtonVariant;
|
confirmVariant?: ButtonVariant;
|
||||||
/** Hide confirm actions when after of them is clicked */
|
/** Hide confirm actions when after of them is clicked */
|
||||||
closeOnConfirm?: boolean;
|
closeOnConfirm?: boolean;
|
||||||
|
/** Move focus to button when mounted */
|
||||||
|
autoFocus?: boolean;
|
||||||
|
|
||||||
/** Optional on click handler for the original button */
|
/** Optional on click handler for the original button */
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@ -33,6 +35,8 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UnThemedConfirmButton extends PureComponent<Props, State> {
|
class UnThemedConfirmButton extends PureComponent<Props, State> {
|
||||||
|
mainButtonRef = React.createRef<HTMLButtonElement>();
|
||||||
|
confirmButtonRef = React.createRef<HTMLButtonElement>();
|
||||||
state: State = {
|
state: State = {
|
||||||
showConfirm: false,
|
showConfirm: false,
|
||||||
};
|
};
|
||||||
@ -42,9 +46,16 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
showConfirm: true,
|
{
|
||||||
});
|
showConfirm: true,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.props.autoFocus && this.confirmButtonRef.current) {
|
||||||
|
this.confirmButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
@ -55,9 +66,14 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
|||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState(
|
||||||
showConfirm: false,
|
{
|
||||||
});
|
showConfirm: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.mainButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
);
|
||||||
if (this.props.onCancel) {
|
if (this.props.onCancel) {
|
||||||
this.props.onCancel();
|
this.props.onCancel();
|
||||||
}
|
}
|
||||||
@ -101,7 +117,7 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
|||||||
<span className={styles.buttonContainer}>
|
<span className={styles.buttonContainer}>
|
||||||
{typeof children === 'string' ? (
|
{typeof children === 'string' ? (
|
||||||
<span className={buttonClass}>
|
<span className={buttonClass}>
|
||||||
<Button size={size} fill="text" onClick={onClick}>
|
<Button size={size} fill="text" onClick={onClick} ref={this.mainButtonRef}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
@ -111,12 +127,12 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={confirmButtonClass}>
|
<span className={confirmButtonClass}>
|
||||||
|
<Button size={size} variant={confirmButtonVariant} onClick={this.onConfirm} ref={this.confirmButtonRef}>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
<Button size={size} fill="text" onClick={this.onClickCancel}>
|
<Button size={size} fill="text" onClick={this.onClickCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size={size} variant={confirmButtonVariant} onClick={this.onConfirm}>
|
|
||||||
{confirmText}
|
|
||||||
</Button>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -128,9 +144,9 @@ export const ConfirmButton = withTheme(UnThemedConfirmButton);
|
|||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
buttonContainer: css`
|
buttonContainer: css`
|
||||||
direction: rtl;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
`,
|
`,
|
||||||
buttonDisabled: css`
|
buttonDisabled: css`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -146,14 +162,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
`,
|
`,
|
||||||
buttonHide: css`
|
buttonHide: css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.1s ease;
|
transition: opacity 0.1s ease, visibility 0 0.1s;
|
||||||
|
visibility: hidden;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
`,
|
`,
|
||||||
confirmButton: css`
|
confirmButton: css`
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background: ${theme.colors.bg1};
|
background: ${theme.colors.bg1};
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`,
|
`,
|
||||||
@ -166,7 +182,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
`,
|
`,
|
||||||
confirmButtonHide: css`
|
confirmButtonHide: css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.12s ease-in, transform 0.14s ease-in;
|
visibility: hidden;
|
||||||
|
transition: opacity 0.12s ease-in, transform 0.14s ease-in, visibility 0s 0.12s;
|
||||||
transform: translateX(100px);
|
transform: translateX(100px);
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
@ -58,8 +58,10 @@ export const ConfirmModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// for some reason autoFocus property did no work on this button, but this does
|
// for some reason autoFocus property did no work on this button, but this does
|
||||||
buttonRef.current?.focus();
|
if (isOpen) {
|
||||||
}, []);
|
buttonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
||||||
|
@ -18,41 +18,48 @@ export interface RadioButtonProps {
|
|||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
'aria-label'?: StringSelector;
|
'aria-label'?: StringSelector;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RadioButton: React.FC<RadioButtonProps> = ({
|
export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
|
||||||
children,
|
(
|
||||||
active = false,
|
{
|
||||||
disabled = false,
|
children,
|
||||||
size = 'md',
|
active = false,
|
||||||
onChange,
|
disabled = false,
|
||||||
id,
|
size = 'md',
|
||||||
name = undefined,
|
onChange,
|
||||||
description,
|
id,
|
||||||
fullWidth,
|
name = undefined,
|
||||||
'aria-label': ariaLabel,
|
description,
|
||||||
}) => {
|
fullWidth,
|
||||||
const theme = useTheme2();
|
'aria-label': ariaLabel,
|
||||||
const styles = getRadioButtonStyles(theme, size, fullWidth);
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getRadioButtonStyles(theme, size, fullWidth);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
className={styles.radio}
|
className={styles.radio}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
checked={active}
|
checked={active}
|
||||||
name={name}
|
name={name}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
/>
|
ref={ref}
|
||||||
<label className={styles.radioLabel} htmlFor={id} title={description}>
|
/>
|
||||||
{children}
|
<label className={styles.radioLabel} htmlFor={id} title={description}>
|
||||||
</label>
|
{children}
|
||||||
</>
|
</label>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
RadioButton.displayName = 'RadioButton';
|
RadioButton.displayName = 'RadioButton';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
@ -16,6 +16,7 @@ export interface RadioButtonGroupProps<T> {
|
|||||||
size?: RadioButtonSize;
|
size?: RadioButtonSize;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RadioButtonGroup<T>({
|
export function RadioButtonGroup<T>({
|
||||||
@ -27,6 +28,7 @@ export function RadioButtonGroup<T>({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
|
autoFocus = false,
|
||||||
}: RadioButtonGroupProps<T>) {
|
}: RadioButtonGroupProps<T>) {
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(option: SelectableValue) => {
|
(option: SelectableValue) => {
|
||||||
@ -42,6 +44,13 @@ export function RadioButtonGroup<T>({
|
|||||||
const groupName = useRef(id);
|
const groupName = useRef(id);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const activeButtonRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus && activeButtonRef.current) {
|
||||||
|
activeButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [autoFocus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.radioGroup, fullWidth && styles.fullWidth, className)}>
|
<div className={cx(styles.radioGroup, fullWidth && styles.fullWidth, className)}>
|
||||||
{options.map((o, i) => {
|
{options.map((o, i) => {
|
||||||
@ -58,6 +67,7 @@ export function RadioButtonGroup<T>({
|
|||||||
name={groupName.current}
|
name={groupName.current}
|
||||||
description={o.description}
|
description={o.description}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
|
ref={value === o.value ? activeButtonRef : undefined}
|
||||||
>
|
>
|
||||||
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
|
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
|
||||||
{o.imgUrl && <img src={o.imgUrl} alt={o.label} className={styles.img} />}
|
{o.imgUrl && <img src={o.imgUrl} alt={o.label} className={styles.img} />}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { AsyncSelect } from '@grafana/ui';
|
import { AsyncSelect } from '@grafana/ui';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { Organization } from 'app/types';
|
import { Organization } from 'app/types';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { useAsyncFn } from 'react-use';
|
||||||
|
|
||||||
export type OrgSelectItem = SelectableValue<Organization>;
|
export type OrgSelectItem = SelectableValue<Organization>;
|
||||||
|
|
||||||
@ -10,56 +11,35 @@ export interface Props {
|
|||||||
onSelected: (org: OrgSelectItem) => void;
|
onSelected: (org: OrgSelectItem) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export function OrgPicker({ onSelected, className, inputId, autoFocus }: Props) {
|
||||||
isLoading: boolean;
|
// For whatever reason the autoFocus prop doesn't seem to work
|
||||||
}
|
// with AsyncSelect, hence this workaround. Maybe fixed in a later version?
|
||||||
|
useEffect(() => {
|
||||||
export class OrgPicker extends PureComponent<Props, State> {
|
if (autoFocus && inputId) {
|
||||||
orgs: Organization[] = [];
|
document.getElementById(inputId)?.focus();
|
||||||
|
|
||||||
state: State = {
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
async loadOrgs() {
|
|
||||||
this.setState({ isLoading: true });
|
|
||||||
const orgs = await getBackendSrv().get('/api/orgs');
|
|
||||||
this.orgs = orgs;
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
return orgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrgOptions = async (query: string): Promise<OrgSelectItem[]> => {
|
|
||||||
if (!this.orgs?.length) {
|
|
||||||
await this.loadOrgs();
|
|
||||||
}
|
}
|
||||||
return this.orgs.map(
|
}, [autoFocus, inputId]);
|
||||||
(org: Organization): OrgSelectItem => ({
|
|
||||||
value: { id: org.id, name: org.name },
|
|
||||||
label: org.name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
const [orgOptionsState, getOrgOptions] = useAsyncFn(async () => {
|
||||||
const { className, onSelected, inputId } = this.props;
|
const orgs: Organization[] = await getBackendSrv().get('/api/orgs');
|
||||||
const { isLoading } = this.state;
|
return orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
inputId={inputId}
|
inputId={inputId}
|
||||||
className={className}
|
className={className}
|
||||||
isLoading={isLoading}
|
isLoading={orgOptionsState.loading}
|
||||||
defaultOptions={true}
|
defaultOptions={true}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
loadOptions={this.getOrgOptions}
|
loadOptions={getOrgOptions}
|
||||||
onChange={onSelected}
|
onChange={onSelected}
|
||||||
placeholder="Select organization"
|
placeholder="Select organization"
|
||||||
noOptionsMessage="No organizations found"
|
noOptionsMessage="No organizations found"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,30 @@ interface Props {
|
|||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
onChange: (role: OrgRole) => void;
|
onChange: (role: OrgRole) => void;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key }));
|
const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key }));
|
||||||
|
|
||||||
export const OrgRolePicker: FC<Props> = ({ value, onChange, 'aria-label': ariaLabel, inputId, ...restProps }) => (
|
export const OrgRolePicker: FC<Props> = ({
|
||||||
<Select
|
value,
|
||||||
menuShouldPortal
|
onChange,
|
||||||
inputId={inputId}
|
'aria-label': ariaLabel,
|
||||||
value={value}
|
inputId,
|
||||||
options={options}
|
autoFocus,
|
||||||
onChange={(val) => onChange(val.value as OrgRole)}
|
...restProps
|
||||||
placeholder="Choose role..."
|
}) => {
|
||||||
aria-label={ariaLabel}
|
return (
|
||||||
{...restProps}
|
<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> {
|
export class UserOrgs extends PureComponent<Props, State> {
|
||||||
|
addToOrgButtonRef = React.createRef<HTMLButtonElement>();
|
||||||
state = {
|
state = {
|
||||||
showAddOrgModal: false,
|
showAddOrgModal: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
showOrgAddModal = (show: boolean) => () => {
|
showOrgAddModal = () => {
|
||||||
this.setState({ showAddOrgModal: show });
|
this.setState({ showAddOrgModal: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
dismissOrgAddModal = () => {
|
||||||
|
this.setState({ showAddOrgModal: false }, () => {
|
||||||
|
this.addToOrgButtonRef.current?.focus();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -69,12 +76,12 @@ export class UserOrgs extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className={addToOrgContainerClass}>
|
<div className={addToOrgContainerClass}>
|
||||||
{canAddToOrg && (
|
{canAddToOrg && (
|
||||||
<Button variant="secondary" onClick={this.showOrgAddModal(true)}>
|
<Button variant="secondary" onClick={this.showOrgAddModal} ref={this.addToOrgButtonRef}>
|
||||||
Add user to organization
|
Add user to organization
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
|
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.dismissOrgAddModal} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -159,7 +166,7 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
|||||||
</td>
|
</td>
|
||||||
{isChangingRole ? (
|
{isChangingRole ? (
|
||||||
<td>
|
<td>
|
||||||
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} />
|
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} autoFocus />
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
<td className="width-25">{org.role}</td>
|
<td className="width-25">{org.role}</td>
|
||||||
@ -184,6 +191,7 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
|||||||
confirmVariant="destructive"
|
confirmVariant="destructive"
|
||||||
onCancel={this.onCancelClick}
|
onCancel={this.onCancelClick}
|
||||||
onConfirm={this.onOrgRemove}
|
onConfirm={this.onOrgRemove}
|
||||||
|
autoFocus
|
||||||
>
|
>
|
||||||
Remove from organization
|
Remove from organization
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
@ -260,7 +268,7 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
|
|||||||
onDismiss={this.onCancel}
|
onDismiss={this.onCancel}
|
||||||
>
|
>
|
||||||
<Field label="Organization">
|
<Field label="Organization">
|
||||||
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} />
|
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} autoFocus />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Role">
|
<Field label="Role">
|
||||||
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
|
<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 { ConfirmButton, RadioButtonGroup, Icon } from '@grafana/ui';
|
||||||
import { cx } from '@emotion/css';
|
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
@ -10,98 +9,72 @@ interface Props {
|
|||||||
onGrafanaAdminChange: (isGrafanaAdmin: boolean) => void;
|
onGrafanaAdminChange: (isGrafanaAdmin: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
|
||||||
isEditing: boolean;
|
|
||||||
currentAdminOption: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminOptions = [
|
const adminOptions = [
|
||||||
{ label: 'Yes', value: 'YES' },
|
{ label: 'Yes', value: true },
|
||||||
{ label: 'No', value: 'NO' },
|
{ label: 'No', value: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
export class UserPermissions extends PureComponent<Props, State> {
|
export function UserPermissions({ isGrafanaAdmin, onGrafanaAdminChange }: Props) {
|
||||||
state = {
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
isEditing: false,
|
const [currentAdminOption, setCurrentAdminOption] = useState(isGrafanaAdmin);
|
||||||
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
|
||||||
|
const onChangeClick = () => setIsEditing(true);
|
||||||
|
|
||||||
|
const onCancelClick = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setCurrentAdminOption(isGrafanaAdmin);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeClick = () => {
|
const handleGrafanaAdminChange = () => onGrafanaAdminChange(currentAdminOption);
|
||||||
this.setState({ isEditing: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancelClick = () => {
|
const canChangePermissions = contextSrv.hasPermission(AccessControlAction.UsersPermissionsUpdate);
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
currentAdminOption: this.props.isGrafanaAdmin ? 'YES' : 'NO',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrafanaAdminChange = () => {
|
return (
|
||||||
const { currentAdminOption } = this.state;
|
<>
|
||||||
const newIsGrafanaAdmin = currentAdminOption === 'YES' ? true : false;
|
<h3 className="page-heading">Permissions</h3>
|
||||||
this.props.onGrafanaAdminChange(newIsGrafanaAdmin);
|
<div className="gf-form-group">
|
||||||
};
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
onAdminOptionSelect = (value: string) => {
|
<tbody>
|
||||||
this.setState({ currentAdminOption: value });
|
<tr>
|
||||||
};
|
<td className="width-16">Grafana Admin</td>
|
||||||
|
{isEditing ? (
|
||||||
render() {
|
<td colSpan={2}>
|
||||||
const { isGrafanaAdmin } = this.props;
|
<RadioButtonGroup
|
||||||
const { isEditing, currentAdminOption } = this.state;
|
options={adminOptions}
|
||||||
const changeButtonContainerClass = cx('pull-right');
|
value={currentAdminOption}
|
||||||
const canChangePermissions = contextSrv.hasPermission(AccessControlAction.UsersPermissionsUpdate);
|
onChange={setCurrentAdminOption}
|
||||||
|
autoFocus
|
||||||
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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
) : (
|
||||||
</tbody>
|
<td colSpan={2}>
|
||||||
</table>
|
{isGrafanaAdmin ? (
|
||||||
</div>
|
<>
|
||||||
|
<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>
|
||||||
</>
|
</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 { AccessControlAction, UserDTO } from 'app/types';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
@ -16,163 +16,149 @@ interface Props {
|
|||||||
onPasswordChange(password: string): void;
|
onPasswordChange(password: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export function UserProfile({
|
||||||
isLoading: boolean;
|
user,
|
||||||
showDeleteModal: boolean;
|
onUserUpdate,
|
||||||
showDisableModal: boolean;
|
onUserDelete,
|
||||||
}
|
onUserDisable,
|
||||||
|
onUserEnable,
|
||||||
|
onPasswordChange,
|
||||||
|
}: Props) {
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [showDisableModal, setShowDisableModal] = useState(false);
|
||||||
|
|
||||||
export class UserProfile extends PureComponent<Props, State> {
|
const deleteUserRef = useRef<HTMLButtonElement | null>(null);
|
||||||
state = {
|
const showDeleteUserModal = (show: boolean) => () => {
|
||||||
isLoading: false,
|
setShowDeleteModal(show);
|
||||||
showDeleteModal: false,
|
if (!show && deleteUserRef.current) {
|
||||||
showDisableModal: false,
|
deleteUserRef.current.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showDeleteUserModal = (show: boolean) => () => {
|
const disableUserRef = useRef<HTMLButtonElement | null>(null);
|
||||||
this.setState({ showDeleteModal: show });
|
const showDisableUserModal = (show: boolean) => () => {
|
||||||
|
setShowDisableModal(show);
|
||||||
|
if (!show && disableUserRef.current) {
|
||||||
|
disableUserRef.current.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showDisableUserModal = (show: boolean) => () => {
|
const handleUserDelete = () => onUserDelete(user.id);
|
||||||
this.setState({ showDisableModal: show });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUserDelete = () => {
|
const handleUserDisable = () => onUserDisable(user.id);
|
||||||
const { user, onUserDelete } = this.props;
|
|
||||||
onUserDelete(user.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUserDisable = () => {
|
const handleUserEnable = () => onUserEnable(user.id);
|
||||||
const { user, onUserDisable } = this.props;
|
|
||||||
onUserDisable(user.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUserEnable = () => {
|
const onUserNameChange = (newValue: string) => {
|
||||||
const { user, onUserEnable } = this.props;
|
|
||||||
onUserEnable(user.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUserNameChange = (newValue: string) => {
|
|
||||||
const { user, onUserUpdate } = this.props;
|
|
||||||
onUserUpdate({
|
onUserUpdate({
|
||||||
...user,
|
...user,
|
||||||
name: newValue,
|
name: newValue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserEmailChange = (newValue: string) => {
|
const onUserEmailChange = (newValue: string) => {
|
||||||
const { user, onUserUpdate } = this.props;
|
|
||||||
onUserUpdate({
|
onUserUpdate({
|
||||||
...user,
|
...user,
|
||||||
email: newValue,
|
email: newValue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserLoginChange = (newValue: string) => {
|
const onUserLoginChange = (newValue: string) => {
|
||||||
const { user, onUserUpdate } = this.props;
|
|
||||||
onUserUpdate({
|
onUserUpdate({
|
||||||
...user,
|
...user,
|
||||||
login: newValue,
|
login: newValue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPasswordChange = (newValue: string) => {
|
const authSource = user.authLabels?.length && user.authLabels[0];
|
||||||
this.props.onPasswordChange(newValue);
|
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||||
};
|
const styles = getStyles(config.theme);
|
||||||
|
|
||||||
render() {
|
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
||||||
const { user } = this.props;
|
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
||||||
const { showDeleteModal, showDisableModal } = this.state;
|
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
||||||
const authSource = user.authLabels?.length && user.authLabels[0];
|
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
||||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
||||||
const styles = getStyles(config.theme);
|
|
||||||
|
|
||||||
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
return (
|
||||||
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
<>
|
||||||
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
<h3 className="page-heading">User information</h3>
|
||||||
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
<div className="gf-form-group">
|
||||||
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
<div className="gf-form">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
return (
|
<tbody>
|
||||||
<>
|
<UserProfileRow
|
||||||
<h3 className="page-heading">User information</h3>
|
label="Name"
|
||||||
<div className="gf-form-group">
|
value={user.name}
|
||||||
<div className="gf-form">
|
locked={editLocked}
|
||||||
<table className="filter-table form-inline">
|
lockMessage={lockMessage}
|
||||||
<tbody>
|
onChange={onUserNameChange}
|
||||||
<UserProfileRow
|
/>
|
||||||
label="Name"
|
<UserProfileRow
|
||||||
value={user.name}
|
label="Email"
|
||||||
locked={editLocked}
|
value={user.email}
|
||||||
lockMessage={lockMessage}
|
locked={editLocked}
|
||||||
onChange={this.onUserNameChange}
|
lockMessage={lockMessage}
|
||||||
/>
|
onChange={onUserEmailChange}
|
||||||
<UserProfileRow
|
/>
|
||||||
label="Email"
|
<UserProfileRow
|
||||||
value={user.email}
|
label="Username"
|
||||||
locked={editLocked}
|
value={user.login}
|
||||||
lockMessage={lockMessage}
|
locked={editLocked}
|
||||||
onChange={this.onUserEmailChange}
|
lockMessage={lockMessage}
|
||||||
/>
|
onChange={onUserLoginChange}
|
||||||
<UserProfileRow
|
/>
|
||||||
label="Username"
|
<UserProfileRow
|
||||||
value={user.login}
|
label="Password"
|
||||||
locked={editLocked}
|
value="********"
|
||||||
lockMessage={lockMessage}
|
inputType="password"
|
||||||
onChange={this.onUserLoginChange}
|
locked={passwordChangeLocked}
|
||||||
/>
|
lockMessage={lockMessage}
|
||||||
<UserProfileRow
|
onChange={onPasswordChange}
|
||||||
label="Password"
|
/>
|
||||||
value="********"
|
</tbody>
|
||||||
inputType="password"
|
</table>
|
||||||
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>
|
|
||||||
</div>
|
</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) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
@ -269,7 +255,6 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
const editButtonContainerClass = cx('pull-right');
|
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
||||||
@ -297,16 +282,14 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className={editButtonContainerClass}>
|
<ConfirmButton
|
||||||
<ConfirmButton
|
confirmText="Save"
|
||||||
confirmText="Save"
|
onClick={this.onEditClick}
|
||||||
onClick={this.onEditClick}
|
onConfirm={this.onSave}
|
||||||
onConfirm={this.onSave}
|
onCancel={this.onCancelClick}
|
||||||
onCancel={this.onCancelClick}
|
>
|
||||||
>
|
Edit
|
||||||
Edit
|
</ConfirmButton>
|
||||||
</ConfirmButton>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -320,13 +303,10 @@ interface LockedRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
|
export const LockedRow: FC<LockedRowProps> = ({ label, value, lockMessage }) => {
|
||||||
const lockMessageClass = cx(
|
const lockMessageClass = css`
|
||||||
'pull-right',
|
font-style: italic;
|
||||||
css`
|
margin-right: 0.6rem;
|
||||||
font-style: italic;
|
`;
|
||||||
margin-right: 0.6rem;
|
|
||||||
`
|
|
||||||
);
|
|
||||||
const labelClass = cx(
|
const labelClass = cx(
|
||||||
'width-16',
|
'width-16',
|
||||||
css`
|
css`
|
||||||
|
@ -16,12 +16,19 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UserSessions extends PureComponent<Props, State> {
|
export class UserSessions extends PureComponent<Props, State> {
|
||||||
|
forceAllLogoutButton = React.createRef<HTMLButtonElement>();
|
||||||
state: State = {
|
state: State = {
|
||||||
showLogoutModal: false,
|
showLogoutModal: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
showLogoutConfirmationModal = (show: boolean) => () => {
|
showLogoutConfirmationModal = () => {
|
||||||
this.setState({ showLogoutModal: show });
|
this.setState({ showLogoutModal: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
dismissLogoutConfirmationModal = () => {
|
||||||
|
this.setState({ showLogoutModal: false }, () => {
|
||||||
|
this.forceAllLogoutButton.current?.focus();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSessionRevoke = (id: number) => {
|
onSessionRevoke = (id: number) => {
|
||||||
@ -74,6 +81,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
confirmText="Confirm logout"
|
confirmText="Confirm logout"
|
||||||
confirmVariant="destructive"
|
confirmVariant="destructive"
|
||||||
onConfirm={this.onSessionRevoke(session.id)}
|
onConfirm={this.onSessionRevoke(session.id)}
|
||||||
|
autoFocus
|
||||||
>
|
>
|
||||||
Force logout
|
Force logout
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
@ -87,7 +95,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className={logoutFromAllDevicesClass}>
|
<div className={logoutFromAllDevicesClass}>
|
||||||
{canLogout && sessions.length > 0 && (
|
{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
|
Force logout from all devices
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -97,7 +105,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
body="Are you sure you want to force logout from all devices?"
|
body="Are you sure you want to force logout from all devices?"
|
||||||
confirmText="Force logout"
|
confirmText="Force logout"
|
||||||
onConfirm={this.onAllSessionsRevoke}
|
onConfirm={this.onAllSessionsRevoke}
|
||||||
onDismiss={this.showLogoutConfirmationModal(false)}
|
onDismiss={this.dismissLogoutConfirmationModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,9 +75,9 @@ describe('ApiKeysPage', () => {
|
|||||||
setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true });
|
setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true });
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
expect(screen.getAllByRole('row').length).toBe(4);
|
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: /first admin 2021-01-01 00:00:00/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00 cancel delete/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 cancel delete/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 },
|
{ id: 3, name: 'Third', role: OrgRole.Viewer, secondsToLive: 0, expiration: undefined },
|
||||||
];
|
];
|
||||||
const { deleteApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true });
|
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 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 cancel delete/i });
|
const secondRow = screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00/i });
|
||||||
|
|
||||||
deleteApiKeyMock.mockClear();
|
deleteApiKeyMock.mockClear();
|
||||||
expect(within(firstRow).getByRole('cell', { name: /cancel delete/i })).toBeInTheDocument();
|
expect(within(firstRow).getByLabelText('Delete API key')).toBeInTheDocument();
|
||||||
userEvent.click(within(firstRow).getByRole('cell', { name: /cancel delete/i }));
|
userEvent.click(within(firstRow).getByLabelText('Delete API key'));
|
||||||
|
|
||||||
expect(within(firstRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument();
|
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 }));
|
||||||
userEvent.click(within(firstRow).getByRole('button', { name: /delete$/i }), undefined, {
|
|
||||||
skipPointerEventsCheck: true,
|
|
||||||
});
|
|
||||||
expect(deleteApiKeyMock).toHaveBeenCalledTimes(1);
|
expect(deleteApiKeyMock).toHaveBeenCalledTimes(1);
|
||||||
expect(deleteApiKeyMock).toHaveBeenCalledWith(1, false);
|
expect(deleteApiKeyMock).toHaveBeenCalledWith(1, false);
|
||||||
|
|
||||||
toggleShowExpired();
|
toggleShowExpired();
|
||||||
|
|
||||||
deleteApiKeyMock.mockClear();
|
deleteApiKeyMock.mockClear();
|
||||||
expect(within(secondRow).getByRole('cell', { name: /cancel delete/i })).toBeInTheDocument();
|
expect(within(secondRow).getByLabelText('Delete API key')).toBeInTheDocument();
|
||||||
userEvent.click(within(secondRow).getByRole('cell', { name: /cancel delete/i }));
|
userEvent.click(within(secondRow).getByLabelText('Delete API key'));
|
||||||
expect(within(secondRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument();
|
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, {
|
userEvent.click(within(secondRow).getByRole('button', { name: /delete$/i }), undefined, {
|
||||||
skipPointerEventsCheck: true,
|
skipPointerEventsCheck: true,
|
||||||
});
|
});
|
||||||
|
@ -116,9 +116,7 @@ describe('AnnotationsSettings', () => {
|
|||||||
|
|
||||||
expect(screen.getByRole('heading', { name: /annotations/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /annotations/i })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('table')).toBeInTheDocument();
|
expect(screen.queryByRole('table')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana/i })).toBeInTheDocument();
|
||||||
screen.getByRole('row', { name: /annotations & alerts \(built\-in\) grafana cancel delete/i })
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@ -142,14 +140,14 @@ describe('AnnotationsSettings', () => {
|
|||||||
userEvent.click(within(heading).getByText(/annotations/i));
|
userEvent.click(within(heading).getByText(/annotations/i));
|
||||||
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
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(
|
expect(
|
||||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: /new query/i })).not.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.getAllByLabelText(/Delete query with title/)[0]);
|
||||||
userEvent.click(screen.getByRole('button', { name: /^delete$/i }), undefined, { skipPointerEventsCheck: true });
|
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||||
|
|
||||||
expect(screen.queryAllByRole('row').length).toBe(0);
|
expect(screen.queryAllByRole('row').length).toBe(0);
|
||||||
expect(
|
expect(
|
||||||
@ -238,9 +236,7 @@ describe('AnnotationsSettings', () => {
|
|||||||
userEvent.click(within(heading).getByText(/annotations/i));
|
userEvent.click(within(heading).getByText(/annotations/i));
|
||||||
|
|
||||||
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
||||||
expect(
|
expect(screen.queryByRole('row', { name: /my prometheus annotation prometheus/i })).toBeInTheDocument();
|
||||||
screen.queryByRole('row', { name: /my prometheus annotation prometheus cancel delete/i })
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole('button', { name: /new query/i })).toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /new query/i })).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByLabelText(selectors.components.CallToActionCard.button('Add annotation query'))
|
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);
|
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.getAllByLabelText(/Delete query with title/)[0]);
|
||||||
userEvent.click(screen.getAllByRole('button', { name: /delete/i })[1], undefined, { skipPointerEventsCheck: true });
|
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||||
|
|
||||||
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(2);
|
||||||
});
|
});
|
||||||
|
@ -131,10 +131,8 @@ describe('LinksSettings', () => {
|
|||||||
|
|
||||||
expect(getTableBodyRows().length).toBe(links.length);
|
expect(getTableBodyRows().length).toBe(links.length);
|
||||||
|
|
||||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
userEvent.click(within(getTableBody()).getAllByLabelText(/Delete link with title/)[0]);
|
||||||
userEvent.click(within(getTableBody()).getAllByRole('button', { name: 'Delete' })[0], undefined, {
|
userEvent.click(within(getTableBody()).getByRole('button', { name: 'Delete' }));
|
||||||
skipPointerEventsCheck: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getTableBodyRows().length).toBe(links.length - 1);
|
expect(getTableBodyRows().length).toBe(links.length - 1);
|
||||||
expect(within(getTableBody()).queryByText(links[0].title)).not.toBeInTheDocument();
|
expect(within(getTableBody()).queryByText(links[0].title)).not.toBeInTheDocument();
|
||||||
|
Loading…
Reference in New Issue
Block a user