mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
A11y: Fix various fastpass accessibility issues (#41154)
This commit is contained in:
@@ -6,14 +6,16 @@ interface Props {
|
|||||||
value: OrgRole;
|
value: OrgRole;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
|
inputId?: string;
|
||||||
onChange: (role: OrgRole) => void;
|
onChange: (role: OrgRole) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
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, ...restProps }) => (
|
export const OrgRolePicker: FC<Props> = ({ value, onChange, 'aria-label': ariaLabel, inputId, ...restProps }) => (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
|
inputId={inputId}
|
||||||
value={value}
|
value={value}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(val) => onChange(val.value as OrgRole)}
|
onChange={(val) => onChange(val.value as OrgRole)}
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
|||||||
invalid={!!errors.name}
|
invalid={!!errors.name}
|
||||||
error={errors.name ? 'Name is required' : undefined}
|
error={errors.name ? 'Name is required' : undefined}
|
||||||
>
|
>
|
||||||
<Input {...register('name', { required: true })} />
|
<Input id="name-input" {...register('name', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Email">
|
<Field label="Email">
|
||||||
<Input {...register('email')} />
|
<Input id="email-input" {...register('email')} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Username">
|
<Field label="Username">
|
||||||
<Input {...register('login')} />
|
<Input id="username-input" {...register('login')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Password"
|
label="Password"
|
||||||
@@ -62,6 +62,7 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
|||||||
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
|
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id="password-input"
|
||||||
{...register('password', {
|
{...register('password', {
|
||||||
validate: (value) => value.trim() !== '' && value.length >= 4,
|
validate: (value) => value.trim() !== '' && value.length >= 4,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -151,12 +151,15 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
|||||||
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
|
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
|
||||||
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
|
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
|
||||||
|
|
||||||
|
const inputId = `${org.name}-input`;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={labelClass}>{org.name}</td>
|
<td className={labelClass}>
|
||||||
|
<label htmlFor={inputId}>{org.name}</label>
|
||||||
|
</td>
|
||||||
{isChangingRole ? (
|
{isChangingRole ? (
|
||||||
<td>
|
<td>
|
||||||
<OrgRolePicker value={currentRole} onChange={this.onOrgRoleChange} />
|
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} />
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
<td className="width-25">{org.role}</td>
|
<td className="width-25">{org.role}</td>
|
||||||
@@ -257,10 +260,10 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
|
|||||||
onDismiss={this.onCancel}
|
onDismiss={this.onCancel}
|
||||||
>
|
>
|
||||||
<Field label="Organization">
|
<Field label="Organization">
|
||||||
<OrgPicker onSelected={this.onOrgSelect} />
|
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Role">
|
<Field label="Role">
|
||||||
<OrgRolePicker value={role} onChange={this.onOrgRoleChange} />
|
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
|
||||||
</Field>
|
</Field>
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<HorizontalGroup spacing="md" justify="center">
|
<HorizontalGroup spacing="md" justify="center">
|
||||||
|
|||||||
@@ -275,12 +275,16 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
|
|||||||
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputId = `${label}-input`;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={labelClass}>{label}</td>
|
<td className={labelClass}>
|
||||||
|
<label htmlFor={inputId}>{label}</label>
|
||||||
|
</td>
|
||||||
<td className="width-25" colSpan={2}>
|
<td className="width-25" colSpan={2}>
|
||||||
{this.state.editing ? (
|
{this.state.editing ? (
|
||||||
<Input
|
<Input
|
||||||
|
id={inputId}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
onBlur={this.onInputBlur}
|
onBlur={this.onInputBlur}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
import AmRoutes from './AmRoutes';
|
import AmRoutes from './AmRoutes';
|
||||||
import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager';
|
import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager';
|
||||||
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
|
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
|
||||||
@@ -76,8 +76,8 @@ const ui = {
|
|||||||
editButton: byRole('button', { name: 'Edit' }),
|
editButton: byRole('button', { name: 'Edit' }),
|
||||||
saveButton: byRole('button', { name: 'Save' }),
|
saveButton: byRole('button', { name: 'Save' }),
|
||||||
|
|
||||||
editRouteButton: byTestId('edit-route'),
|
editRouteButton: byLabelText('Edit route'),
|
||||||
deleteRouteButton: byTestId('delete-route'),
|
deleteRouteButton: byLabelText('Delete route'),
|
||||||
newPolicyButton: byRole('button', { name: /New policy/ }),
|
newPolicyButton: byRole('button', { name: /New policy/ }),
|
||||||
|
|
||||||
receiverSelect: byTestId('am-receiver-select'),
|
receiverSelect: byTestId('am-receiver-select'),
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ export const CollapseToggle: FC<Props> = ({ isCollapsed, onToggle, className, te
|
|||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={cx(styles.expandButton, className)} onClick={() => onToggle(!isCollapsed)} {...restOfProps}>
|
<button
|
||||||
|
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} alert group`}
|
||||||
|
className={cx(styles.expandButton, className)}
|
||||||
|
onClick={() => onToggle(!isCollapsed)}
|
||||||
|
{...restOfProps}
|
||||||
|
>
|
||||||
<Icon size={size} name={isCollapsed ? 'angle-right' : 'angle-down'} />
|
<Icon size={size} name={isCollapsed ? 'angle-right' : 'angle-down'} />
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const DynamicTable = <T extends object>({
|
|||||||
{isExpandable && (
|
{isExpandable && (
|
||||||
<div className={cx(styles.cell, styles.expandCell)}>
|
<div className={cx(styles.cell, styles.expandCell)}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
|
||||||
size="xl"
|
size="xl"
|
||||||
data-testid="collapse-toggle"
|
data-testid="collapse-toggle"
|
||||||
className={styles.expandButton}
|
className={styles.expandButton}
|
||||||
|
|||||||
@@ -42,11 +42,12 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
aria-label="Default contact point"
|
||||||
{...field}
|
{...field}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={receivers}
|
options={receivers}
|
||||||
|
menuShouldPortal
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -72,6 +73,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
aria-label="Group by"
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
{...field}
|
{...field}
|
||||||
allowCustomValue
|
allowCustomValue
|
||||||
@@ -129,6 +131,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
className={styles.input}
|
className={styles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Group wait type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -169,6 +172,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
className={styles.input}
|
className={styles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Group interval type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -205,6 +209,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Repeat interval type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
className={styles.matchersOperator}
|
className={styles.matchersOperator}
|
||||||
onChange={(value) => onChange(value?.value)}
|
onChange={(value) => onChange(value?.value)}
|
||||||
options={matcherFieldOptions}
|
options={matcherFieldOptions}
|
||||||
|
aria-label="Operator"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
defaultValue={field.operator}
|
defaultValue={field.operator}
|
||||||
@@ -127,11 +128,12 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
aria-label="Contact point"
|
||||||
{...field}
|
{...field}
|
||||||
className={formStyles.input}
|
className={formStyles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={receivers}
|
options={receivers}
|
||||||
|
menuShouldPortal
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -139,10 +141,11 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Continue matching subsequent sibling nodes">
|
<Field label="Continue matching subsequent sibling nodes">
|
||||||
<Switch {...register('continue')} />
|
<Switch id="continue-toggle" {...register('continue')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Override grouping">
|
<Field label="Override grouping">
|
||||||
<Switch
|
<Switch
|
||||||
|
id="override-grouping-toggle"
|
||||||
value={overrideGrouping}
|
value={overrideGrouping}
|
||||||
onChange={() => setOverrideGrouping((overrideGrouping) => !overrideGrouping)}
|
onChange={() => setOverrideGrouping((overrideGrouping) => !overrideGrouping)}
|
||||||
/>
|
/>
|
||||||
@@ -152,6 +155,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
aria-label="Group by"
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
{...field}
|
{...field}
|
||||||
allowCustomValue
|
allowCustomValue
|
||||||
@@ -173,6 +177,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
)}
|
)}
|
||||||
<Field label="Override general timings">
|
<Field label="Override general timings">
|
||||||
<Switch
|
<Switch
|
||||||
|
id="override-timings-toggle"
|
||||||
value={overrideTimings}
|
value={overrideTimings}
|
||||||
onChange={() => setOverrideTimings((overrideTimings) => !overrideTimings)}
|
onChange={() => setOverrideTimings((overrideTimings) => !overrideTimings)}
|
||||||
/>
|
/>
|
||||||
@@ -189,7 +194,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field, fieldState: { invalid } }) => (
|
render={({ field, fieldState: { invalid } }) => (
|
||||||
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
className={formStyles.smallInput}
|
||||||
|
invalid={invalid}
|
||||||
|
placeholder="Time"
|
||||||
|
aria-label="Group wait value"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="groupWaitValue"
|
name="groupWaitValue"
|
||||||
@@ -205,6 +216,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
className={formStyles.input}
|
className={formStyles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Group wait type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -223,7 +235,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field, fieldState: { invalid } }) => (
|
render={({ field, fieldState: { invalid } }) => (
|
||||||
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
className={formStyles.smallInput}
|
||||||
|
invalid={invalid}
|
||||||
|
placeholder="Time"
|
||||||
|
aria-label="Group interval value"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="groupIntervalValue"
|
name="groupIntervalValue"
|
||||||
@@ -239,6 +257,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
className={formStyles.input}
|
className={formStyles.input}
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Group interval type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -257,7 +276,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
<div className={cx(formStyles.container, formStyles.timingContainer)}>
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field, fieldState: { invalid } }) => (
|
render={({ field, fieldState: { invalid } }) => (
|
||||||
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
className={formStyles.smallInput}
|
||||||
|
invalid={invalid}
|
||||||
|
placeholder="Time"
|
||||||
|
aria-label="Repeat interval value"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="repeatIntervalValue"
|
name="repeatIntervalValue"
|
||||||
@@ -274,6 +299,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
|
|||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||||
options={timeOptions}
|
options={timeOptions}
|
||||||
|
aria-label="Repeat interval type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
return (
|
return (
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Button
|
<Button
|
||||||
data-testid="edit-route"
|
aria-label="Edit route"
|
||||||
icon="pen"
|
icon="pen"
|
||||||
onClick={expandWithCustomContent}
|
onClick={expandWithCustomContent}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -86,7 +86,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
<IconButton
|
||||||
data-testid="delete-route"
|
aria-label="Delete route"
|
||||||
name="trash-alt"
|
name="trash-alt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newRoutes = [...routes];
|
const newRoutes = [...routes];
|
||||||
|
|||||||
@@ -72,17 +72,19 @@ export function ChannelSubForm<R extends ChannelValues>({
|
|||||||
const mandatoryOptions = notifier?.options.filter((o) => o.required);
|
const mandatoryOptions = notifier?.options.filter((o) => o.required);
|
||||||
const optionalOptions = notifier?.options.filter((o) => !o.required);
|
const optionalOptions = notifier?.options.filter((o) => !o.required);
|
||||||
|
|
||||||
|
const contactPointTypeInputId = `contact-point-type-${pathPrefix}`;
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} data-testid="item-container">
|
<div className={styles.wrapper} data-testid="item-container">
|
||||||
<div className={styles.topRow}>
|
<div className={styles.topRow}>
|
||||||
<div>
|
<div>
|
||||||
<Field label="Contact point type" data-testid={`${pathPrefix}type`}>
|
<Field label="Contact point type" htmlFor={contactPointTypeInputId} data-testid={`${pathPrefix}type`}>
|
||||||
<InputControl
|
<InputControl
|
||||||
name={name('type')}
|
name={name('type')}
|
||||||
defaultValue={defaultValues.type}
|
defaultValue={defaultValues.type}
|
||||||
render={({ field: { ref, onChange, ...field } }) => (
|
render={({ field: { ref, onChange, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
|
inputId={contactPointTypeInputId}
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
{...field}
|
{...field}
|
||||||
width={37}
|
width={37}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
|
aria-label="Rule type"
|
||||||
{...field}
|
{...field}
|
||||||
options={alertTypeOptions}
|
options={alertTypeOptions}
|
||||||
onChange={(v: SelectableValue) => onChange(v?.value)}
|
onChange={(v: SelectableValue) => onChange(v?.value)}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ interface Props {
|
|||||||
value?: string;
|
value?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, ...rest }) => {
|
export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, 'aria-label': ariaLabel, ...rest }) => {
|
||||||
const annotationOptions = useMemo(
|
const annotationOptions = useMemo(
|
||||||
(): SelectableValue[] =>
|
(): SelectableValue[] =>
|
||||||
Object.values(Annotation)
|
Object.values(Annotation)
|
||||||
@@ -23,6 +24,7 @@ export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, ...rest })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectWithAdd
|
<SelectWithAdd
|
||||||
|
aria-label={ariaLabel}
|
||||||
value={value}
|
value={value}
|
||||||
options={annotationOptions}
|
options={annotationOptions}
|
||||||
custom={!!value && !(Object.values(Annotation) as string[]).includes(value)}
|
custom={!!value && !(Object.values(Annotation) as string[]).includes(value)}
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ const AnnotationsField: FC = () => {
|
|||||||
<InputControl
|
<InputControl
|
||||||
name={`annotations[${index}].key`}
|
name={`annotations[${index}].key`}
|
||||||
render={({ field: { ref, ...field } }) => (
|
render={({ field: { ref, ...field } }) => (
|
||||||
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={18} />
|
<AnnotationKeyInput
|
||||||
|
{...field}
|
||||||
|
aria-label={`Annotation detail ${index + 1}`}
|
||||||
|
existingKeys={existingKeys(index)}
|
||||||
|
width={18}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ConditionField: FC = () => {
|
|||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
|
aria-label="Condition"
|
||||||
{...field}
|
{...field}
|
||||||
width={42}
|
width={42}
|
||||||
options={options}
|
options={options}
|
||||||
|
|||||||
@@ -51,12 +51,19 @@ export const GrafanaConditionsStep: FC = () => {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
|
const evaluateEveryId = 'eval-every-input';
|
||||||
|
const evaluateForId = 'eval-for-input';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||||
<ConditionField />
|
<ConditionField />
|
||||||
<Field label="Evaluate">
|
<Field label="Evaluate">
|
||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
<InlineLabel width={16} tooltip="How often the alert will be evaluated to see if it fires">
|
<InlineLabel
|
||||||
|
htmlFor={evaluateEveryId}
|
||||||
|
width={16}
|
||||||
|
tooltip="How often the alert will be evaluated to see if it fires"
|
||||||
|
>
|
||||||
Evaluate every
|
Evaluate every
|
||||||
</InlineLabel>
|
</InlineLabel>
|
||||||
<Field
|
<Field
|
||||||
@@ -65,9 +72,10 @@ export const GrafanaConditionsStep: FC = () => {
|
|||||||
invalid={!!errors.evaluateEvery?.message}
|
invalid={!!errors.evaluateEvery?.message}
|
||||||
validationMessageHorizontalOverflow={true}
|
validationMessageHorizontalOverflow={true}
|
||||||
>
|
>
|
||||||
<Input width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
|
<Input id={evaluateEveryId} width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
|
||||||
</Field>
|
</Field>
|
||||||
<InlineLabel
|
<InlineLabel
|
||||||
|
htmlFor={evaluateForId}
|
||||||
width={7}
|
width={7}
|
||||||
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
|
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
|
||||||
>
|
>
|
||||||
@@ -79,7 +87,7 @@ export const GrafanaConditionsStep: FC = () => {
|
|||||||
invalid={!!errors.evaluateFor?.message}
|
invalid={!!errors.evaluateFor?.message}
|
||||||
validationMessageHorizontalOverflow={true}
|
validationMessageHorizontalOverflow={true}
|
||||||
>
|
>
|
||||||
<Input width={8} {...register('evaluateFor', forValidationOptions)} />
|
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions)} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -92,11 +100,12 @@ export const GrafanaConditionsStep: FC = () => {
|
|||||||
/>
|
/>
|
||||||
{showErrorHandling && (
|
{showErrorHandling && (
|
||||||
<>
|
<>
|
||||||
<Field label="Alert state if no data or all values are null">
|
<Field htmlFor="no-data-state-input" label="Alert state if no data or all values are null">
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<GrafanaAlertStatePicker
|
<GrafanaAlertStatePicker
|
||||||
{...field}
|
{...field}
|
||||||
|
inputId="no-data-state-input"
|
||||||
width={42}
|
width={42}
|
||||||
includeNoData={true}
|
includeNoData={true}
|
||||||
onChange={(value) => onChange(value?.value)}
|
onChange={(value) => onChange(value?.value)}
|
||||||
@@ -105,11 +114,12 @@ export const GrafanaConditionsStep: FC = () => {
|
|||||||
name="noDataState"
|
name="noDataState"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Alert state if execution error or timeout">
|
<Field htmlFor="exec-err-state-input" label="Alert state if execution error or timeout">
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<GrafanaAlertStatePicker
|
<GrafanaAlertStatePicker
|
||||||
{...field}
|
{...field}
|
||||||
|
inputId="exec-err-state-input"
|
||||||
width={42}
|
width={42}
|
||||||
includeNoData={false}
|
includeNoData={false}
|
||||||
onChange={(value) => onChange(value?.value)}
|
onChange={(value) => onChange(value?.value)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface Props {
|
|||||||
onCustomChange?: (custom: boolean) => void;
|
onCustomChange?: (custom: boolean) => void;
|
||||||
width?: number;
|
width?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
'aria-label'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectWithAdd: FC<Props> = ({
|
export const SelectWithAdd: FC<Props> = ({
|
||||||
@@ -26,6 +27,7 @@ export const SelectWithAdd: FC<Props> = ({
|
|||||||
onCustomChange,
|
onCustomChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
addLabel = '+ Add new',
|
addLabel = '+ Add new',
|
||||||
|
'aria-label': ariaLabel,
|
||||||
}) => {
|
}) => {
|
||||||
const [isCustom, setIsCustom] = useState(custom);
|
const [isCustom, setIsCustom] = useState(custom);
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ export const SelectWithAdd: FC<Props> = ({
|
|||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={ariaLabel}
|
||||||
width={width}
|
width={width}
|
||||||
autoFocus={!custom}
|
autoFocus={!custom}
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
@@ -56,6 +59,7 @@ export const SelectWithAdd: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
|
aria-label={ariaLabel}
|
||||||
width={width}
|
width={width}
|
||||||
options={_options}
|
options={_options}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ export const ActionIcon: FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const iconEl = <Icon className={cx(useStyles(getStyle), className)} onClick={onClick} name={icon} {...rest} />;
|
const iconEl = <Icon className={cx(useStyles(getStyle), className)} onClick={onClick} name={icon} {...rest} />;
|
||||||
|
|
||||||
|
const ariaLabel = typeof tooltip === 'string' ? tooltip : undefined;
|
||||||
return (
|
return (
|
||||||
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (to) {
|
if (to) {
|
||||||
return (
|
return (
|
||||||
<Link to={to} target={target}>
|
<Link aria-label={ariaLabel} to={to} target={target}>
|
||||||
{iconEl}
|
{iconEl}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function RuleDetailsDataSources(props: Props): JSX.Element | null {
|
|||||||
<div key={name}>
|
<div key={name}>
|
||||||
{icon && (
|
{icon && (
|
||||||
<>
|
<>
|
||||||
<img className={styles.dataSourceIcon} src={icon} />{' '}
|
<img alt={`${name} datasource logo`} className={styles.dataSourceIcon} src={icon} />{' '}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
Reference in New Issue
Block a user