Alerting: a11y improvements (#63072)

This commit is contained in:
Konrad Lalik 2023-02-10 10:23:40 +01:00 committed by GitHub
parent c70571c536
commit 53a8998c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 109 deletions

View File

@ -4,20 +4,22 @@ import React from 'react';
import { PanelModel } from '../dashboard/state'; import { PanelModel } from '../dashboard/state';
import { createDashboardModelFixture, createPanelJSONFixture } from '../dashboard/state/__fixtures__/dashboardFixtures'; import { createDashboardModelFixture, createPanelJSONFixture } from '../dashboard/state/__fixtures__/dashboardFixtures';
import { TestRuleResult, Props } from './TestRuleResult'; import { TestRuleResult } from './TestRuleResult';
const backendSrv = {
post: jest.fn(),
};
jest.mock('@grafana/runtime', () => { jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime'); const original = jest.requireActual('@grafana/runtime');
return { return {
...original, ...original,
getBackendSrv: () => ({ getBackendSrv: () => backendSrv,
post: jest.fn(),
}),
}; };
}); });
const props: Props = { const props: React.ComponentProps<typeof TestRuleResult> = {
panel: new PanelModel({ id: 1 }), panel: new PanelModel({ id: 1 }),
dashboard: createDashboardModelFixture({ dashboard: createDashboardModelFixture({
panels: [createPanelJSONFixture({ id: 1 })], panels: [createPanelJSONFixture({ id: 1 })],
@ -30,9 +32,14 @@ describe('TestRuleResult', () => {
}); });
it('should call testRule when mounting', () => { it('should call testRule when mounting', () => {
jest.spyOn(TestRuleResult.prototype, 'testRule'); jest.spyOn(backendSrv, 'post');
render(<TestRuleResult {...props} />); render(<TestRuleResult {...props} />);
expect(TestRuleResult.prototype.testRule).toHaveBeenCalled(); expect(backendSrv.post).toHaveBeenCalledWith(
'/api/alerts/test',
expect.objectContaining({
panelId: 1,
})
);
}); });
}); });

View File

@ -1,11 +1,20 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { LoadingPlaceholder, JSONFormatter, Icon, HorizontalGroup, ClipboardButton } from '@grafana/ui'; import {
LoadingPlaceholder,
JSONFormatter,
Icon,
HorizontalGroup,
ClipboardButton,
clearButtonStyles,
withTheme2,
Themeable2,
} from '@grafana/ui';
import { DashboardModel, PanelModel } from '../dashboard/state'; import { DashboardModel, PanelModel } from '../dashboard/state';
export interface Props { export interface Props extends Themeable2 {
dashboard: DashboardModel; dashboard: DashboardModel;
panel: PanelModel; panel: PanelModel;
} }
@ -16,7 +25,7 @@ interface State {
testRuleResponse: {}; testRuleResponse: {};
} }
export class TestRuleResult extends PureComponent<Props, State> { class UnThemedTestRuleResult extends PureComponent<Props, State> {
readonly state: State = { readonly state: State = {
isLoading: false, isLoading: false,
allNodesExpanded: null, allNodesExpanded: null,
@ -90,6 +99,7 @@ export class TestRuleResult extends PureComponent<Props, State> {
render() { render() {
const { testRuleResponse, isLoading } = this.state; const { testRuleResponse, isLoading } = this.state;
const clearButton = clearButtonStyles(this.props.theme);
if (isLoading === true) { if (isLoading === true) {
return <LoadingPlaceholder text="Evaluating rule" />; return <LoadingPlaceholder text="Evaluating rule" />;
@ -101,7 +111,9 @@ export class TestRuleResult extends PureComponent<Props, State> {
<> <>
<div className="pull-right"> <div className="pull-right">
<HorizontalGroup spacing="md"> <HorizontalGroup spacing="md">
<div onClick={this.onToggleExpand}>{this.renderExpandCollapse()}</div> <button type="button" className={clearButton} onClick={this.onToggleExpand}>
{this.renderExpandCollapse()}
</button>
<ClipboardButton getText={this.getTextForClipboard} icon="copy"> <ClipboardButton getText={this.getTextForClipboard} icon="copy">
Copy to Clipboard Copy to Clipboard
</ClipboardButton> </ClipboardButton>
@ -113,3 +125,5 @@ export class TestRuleResult extends PureComponent<Props, State> {
); );
} }
} }
export const TestRuleResult = withTheme2(UnThemedTestRuleResult);

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Badge, useStyles2 } from '@grafana/ui'; import { Badge, clearButtonStyles, useStyles2 } from '@grafana/ui';
interface AlertConditionProps { interface AlertConditionProps {
enabled?: boolean; enabled?: boolean;
@ -33,22 +33,27 @@ export const AlertConditionIndicator: FC<AlertConditionProps> = ({
if (!enabled) { if (!enabled) {
return ( return (
<div className={styles.actionLink} onClick={() => onSetCondition && onSetCondition()}> <button type="button" className={styles.actionLink} onClick={() => onSetCondition && onSetCondition()}>
Make this the alert condition Make this the alert condition
</div> </button>
); );
} }
return null; return null;
}; };
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => {
actionLink: css` const clearButton = clearButtonStyles(theme);
color: ${theme.colors.text.link};
cursor: pointer;
&:hover { return {
text-decoration: underline; actionLink: css`
} ${clearButton};
`, color: ${theme.colors.text.link};
}); cursor: pointer;
&:hover {
text-decoration: underline;
}
`,
};
};

View File

@ -5,7 +5,7 @@ import React, { FC, useCallback, useState } from 'react';
import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
import { isTimeSeries } from '@grafana/data/src/dataframe/utils'; import { isTimeSeries } from '@grafana/data/src/dataframe/utils';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { AutoSizeInput, Icon, IconButton, Select, useStyles2 } from '@grafana/ui'; import { AutoSizeInput, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui';
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
import { Math } from 'app/features/expressions/components/Math'; import { Math } from 'app/features/expressions/components/Math';
import { Reduce } from 'app/features/expressions/components/Reduce'; import { Reduce } from 'app/features/expressions/components/Reduce';
@ -175,6 +175,7 @@ interface HeaderProps {
const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpressionType, onRemoveExpression }) => { const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpressionType, onRemoveExpression }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const clearButton = useStyles2(clearButtonStyles);
/** /**
* There are 3 edit modes: * There are 3 edit modes:
* *
@ -195,9 +196,9 @@ const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpr
<Stack direction="row" gap={0.5} alignItems="center"> <Stack direction="row" gap={0.5} alignItems="center">
<Stack direction="row" gap={1} alignItems="center" wrap={false}> <Stack direction="row" gap={1} alignItems="center" wrap={false}>
{!editingRefId && ( {!editingRefId && (
<div className={styles.editable} onClick={() => setEditMode('refId')}> <button type="button" className={cx(clearButton, styles.editable)} onClick={() => setEditMode('refId')}>
<div className={styles.expression.refId}>{refId}</div> <div className={styles.expression.refId}>{refId}</div>
</div> </button>
)} )}
{editingRefId && ( {editingRefId && (
<AutoSizeInput <AutoSizeInput
@ -216,10 +217,14 @@ const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpr
/> />
)} )}
{!editingType && ( {!editingType && (
<div className={styles.editable} onClick={() => setEditMode('expressionType')}> <button
type="button"
className={cx(clearButton, styles.editable)}
onClick={() => setEditMode('expressionType')}
>
<div className={styles.mutedText}>{capitalize(queryType)}</div> <div className={styles.mutedText}>{capitalize(queryType)}</div>
<Icon size="xs" name="pen" className={styles.mutedIcon} onClick={() => setEditMode('expressionType')} /> <Icon size="xs" name="pen" className={styles.mutedIcon} onClick={() => setEditMode('expressionType')} />
</div> </button>
)} )}
{editingType && ( {editingType && (
<Select <Select

View File

@ -27,12 +27,15 @@ export const CollapsibleSection = ({
return ( return (
<div className={cx(styles.wrapper, className)}> <div className={cx(styles.wrapper, className)}>
<div className={styles.heading} onClick={toggleCollapse}> <CollapseToggle
<CollapseToggle className={styles.caret} size={size} onToggle={toggleCollapse} isCollapsed={isCollapsed} /> className={styles.toggle}
<h6>{label}</h6> size={size}
</div> onToggle={toggleCollapse}
isCollapsed={isCollapsed}
text={label}
/>
{description && <p className={styles.description}>{description}</p>} {description && <p className={styles.description}>{description}</p>}
<div className={isCollapsed ? styles.hidden : undefined}>{children}</div> <div className={isCollapsed ? styles.hidden : styles.content}>{children}</div>
</div> </div>
); );
}; };
@ -42,14 +45,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
margin-top: ${theme.spacing(1)}; margin-top: ${theme.spacing(1)};
padding-bottom: ${theme.spacing(1)}; padding-bottom: ${theme.spacing(1)};
`, `,
caret: css` toggle: css`
margin-left: -${theme.spacing(0.5)}; // make it align with fields despite icon size margin: ${theme.spacing(1, 0)};
`, padding: 0;
heading: css`
cursor: pointer;
h6 {
display: inline-block;
}
`, `,
hidden: css` hidden: css`
display: none; display: none;
@ -60,4 +58,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
font-weight: ${theme.typography.fontWeightRegular}; font-weight: ${theme.typography.fontWeightRegular};
margin: 0; margin: 0;
`, `,
content: css`
padding-left: ${theme.spacing(3)};
`,
}); });

View File

@ -5,7 +5,16 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data/src'; import { GrafanaTheme2 } from '@grafana/data/src';
import { FilterInput, LoadingPlaceholder, useStyles2, Icon, Modal, Button, Alert } from '@grafana/ui'; import {
FilterInput,
LoadingPlaceholder,
useStyles2,
Icon,
Modal,
Button,
Alert,
clearButtonStyles,
} from '@grafana/ui';
import { dashboardApi } from '../../api/dashboardApi'; import { dashboardApi } from '../../api/dashboardApi';
@ -99,17 +108,18 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
const isSelected = selectedDashboardUid === dashboard.uid; const isSelected = selectedDashboardUid === dashboard.uid;
return ( return (
<div <button
type="button"
title={dashboard.title} title={dashboard.title}
style={style} style={style}
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })} className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
onClick={() => handleDashboardChange(dashboard.uid)} onClick={() => handleDashboardChange(dashboard.uid)}
> >
<div className={styles.dashboardTitle}>{dashboard.title}</div> <div className={styles.dashboardTitle}>{dashboard.title}</div>
<div className={styles.dashboardFolder}> <div className={styles.dashboardFolder}>
<Icon name="folder" /> {dashboard.folderTitle ?? 'General'} <Icon name="folder" /> {dashboard.folderTitle ?? 'General'}
</div> </div>
</div> </button>
); );
}; };
@ -118,13 +128,14 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
const isSelected = selectedPanelId === panel.id.toString(); const isSelected = selectedPanelId === panel.id.toString();
return ( return (
<div <button
type="button"
style={style} style={style}
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })} className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
onClick={() => setSelectedPanelId(panel.id.toString())} onClick={() => setSelectedPanelId(panel.id.toString())}
> >
{panel.title || '<No title>'} {panel.title || '<No title>'}
</div> </button>
); );
}; };
@ -221,60 +232,66 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
); );
}; };
const getPickerStyles = (theme: GrafanaTheme2) => ({ const getPickerStyles = (theme: GrafanaTheme2) => {
container: css` const clearButton = clearButtonStyles(theme);
display: grid;
grid-template-columns: 1fr 1fr; return {
grid-template-rows: min-content auto; container: css`
gap: ${theme.spacing(2)}; display: grid;
flex: 1; grid-template-columns: 1fr 1fr;
`, grid-template-rows: min-content auto;
column: css` gap: ${theme.spacing(2)};
flex: 1 1 auto; flex: 1;
`, `,
dashboardTitle: css` column: css`
height: 22px; flex: 1 1 auto;
font-weight: ${theme.typography.fontWeightBold}; `,
`, dashboardTitle: css`
dashboardFolder: css` height: 22px;
height: 20px; font-weight: ${theme.typography.fontWeightBold};
font-size: ${theme.typography.bodySmall.fontSize}; `,
color: ${theme.colors.text.secondary}; dashboardFolder: css`
display: flex; height: 20px;
flex-direction: row; font-size: ${theme.typography.bodySmall.fontSize};
justify-content: flex-start; color: ${theme.colors.text.secondary};
column-gap: ${theme.spacing(1)}; display: flex;
align-items: center; flex-direction: row;
`, justify-content: flex-start;
row: css` column-gap: ${theme.spacing(1)};
padding: ${theme.spacing(0.5)}; align-items: center;
overflow: hidden; `,
text-overflow: ellipsis; rowButton: css`
white-space: nowrap; ${clearButton};
cursor: pointer; padding: ${theme.spacing(0.5)};
border: 2px solid transparent; overflow: hidden;
`, text-overflow: ellipsis;
rowSelected: css` text-align: left;
border-color: ${theme.colors.primary.border}; white-space: nowrap;
`, cursor: pointer;
rowOdd: css` border: 2px solid transparent;
background-color: ${theme.colors.background.secondary}; `,
`, rowSelected: css`
loadingPlaceholder: css` border-color: ${theme.colors.primary.border};
height: 100%; `,
display: flex; rowOdd: css`
justify-content: center; background-color: ${theme.colors.background.secondary};
align-items: center; `,
`, loadingPlaceholder: css`
modal: css` height: 100%;
height: 100%; display: flex;
`, justify-content: center;
modalContent: css` align-items: center;
flex: 1; `,
display: flex; modal: css`
flex-direction: column; height: 100%;
`, `,
modalAlert: css` modalContent: css`
flex-grow: 0; flex: 1;
`, display: flex;
}); flex-direction: column;
`,
modalAlert: css`
flex-grow: 0;
`,
};
};

View File

@ -1,10 +1,10 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { noop } from 'lodash'; import { noop } from 'lodash';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui'; import { clearButtonStyles, Icon, useStyles2 } from '@grafana/ui';
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable'; import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
import { Alert } from 'app/types/unified-alerting'; import { Alert } from 'app/types/unified-alerting';
@ -24,6 +24,7 @@ export const AlertInstances = ({ alerts, options }: Props) => {
const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances; const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances;
const [displayInstances, setDisplayInstances] = useState<boolean>(defaultShowInstances); const [displayInstances, setDisplayInstances] = useState<boolean>(defaultShowInstances);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const clearButton = useStyles2(clearButtonStyles);
const toggleDisplayInstances = useCallback(() => { const toggleDisplayInstances = useCallback(() => {
setDisplayInstances((display) => !display); setDisplayInstances((display) => !display);
@ -50,11 +51,14 @@ export const AlertInstances = ({ alerts, options }: Props) => {
return ( return (
<div> <div>
{options.groupMode === GroupMode.Default && ( {options.groupMode === GroupMode.Default && (
<div className={uncollapsible ? styles.clickable : ''} onClick={() => toggleShowInstances()}> <button
className={cx(clearButton, uncollapsible ? styles.clickable : '')}
onClick={() => toggleShowInstances()}
>
{uncollapsible && <Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} />} {uncollapsible && <Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} />}
<span>{`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}</span> <span>{`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}</span>
{hiddenInstances > 0 && <span>, {`${hiddenInstances} hidden by filters`}</span>} {hiddenInstances > 0 && <span>, {`${hiddenInstances} hidden by filters`}</span>}
</div> </button>
)} )}
{displayInstances && ( {displayInstances && (
<AlertInstancesTable <AlertInstancesTable