A11y: Make Annotations and Template Variables list and edit pages responsive (#71791)

This commit is contained in:
Juan Cabanas 2023-07-28 10:09:31 -03:00 committed by GitHub
parent c4731efb62
commit 66cea5aac6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 87 deletions

View File

@ -27,7 +27,6 @@ export const CallToActionCard = ({ message, callToActionElement, footer, classNa
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({ wrapper: css({
label: 'call-to-action-card', label: 'call-to-action-card',
padding: theme.spacing(3),
background: theme.colors.background.secondary, background: theme.colors.background.secondary,
borderRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
display: 'flex', display: 'flex',
@ -35,6 +34,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexGrow: 1, flexGrow: 1,
padding: theme.spacing(3),
[theme.breakpoints.down('sm')]: {
padding: theme.spacing(3, 1),
},
}), }),
message: css({ message: css({
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),

View File

@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { arrayUtils, AnnotationQuery } from '@grafana/data'; import { arrayUtils, AnnotationQuery } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { Button, DeleteButton, IconButton, VerticalGroup } from '@grafana/ui'; import { Button, DeleteButton, IconButton, useStyles2, VerticalGroup } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
@ -15,6 +16,7 @@ type Props = {
}; };
export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => { export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => {
const styles = useStyles2(getStyles);
const [annotations, updateAnnotations] = useState(dashboard.annotations.list); const [annotations, updateAnnotations] = useState(dashboard.annotations.list);
const onMove = (idx: number, direction: number) => { const onMove = (idx: number, direction: number) => {
@ -53,54 +55,56 @@ export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => {
return ( return (
<VerticalGroup> <VerticalGroup>
{annotations.length > 0 && ( {annotations.length > 0 && (
<table role="grid" className="filter-table filter-table--hover"> <div className={styles.table}>
<thead> <table role="grid" className="filter-table filter-table--hover">
<tr> <thead>
<th>Query name</th> <tr>
<th>Data source</th> <th>Query name</th>
<th colSpan={3}></th> <th>Data source</th>
</tr> <th colSpan={3}></th>
</thead>
<tbody>
{dashboard.annotations.list.map((annotation, idx) => (
<tr key={`${annotation.name}-${idx}`}>
{annotation.builtIn ? (
<td role="gridcell" style={{ width: '90%' }} className="pointer" onClick={() => onEdit(idx)}>
<Button size="sm" fill="text" variant="secondary">
{getAnnotationName(annotation)}
</Button>
</td>
) : (
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
<Button size="sm" fill="text" variant="secondary">
{getAnnotationName(annotation)}
</Button>
</td>
)}
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{idx !== 0 && <IconButton name="arrow-up" onClick={() => onMove(idx, -1)} tooltip="Move up" />}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{dashboard.annotations.list.length > 1 && idx !== dashboard.annotations.list.length - 1 ? (
<IconButton name="arrow-down" onClick={() => onMove(idx, 1)} tooltip="Move down" />
) : null}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{!annotation.builtIn && (
<DeleteButton
size="sm"
onConfirm={() => onDelete(idx)}
aria-label={`Delete query with title "${annotation.name}"`}
/>
)}
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {dashboard.annotations.list.map((annotation, idx) => (
<tr key={`${annotation.name}-${idx}`}>
{annotation.builtIn ? (
<td role="gridcell" style={{ width: '90%' }} className="pointer" onClick={() => onEdit(idx)}>
<Button size="sm" fill="text" variant="secondary">
{getAnnotationName(annotation)}
</Button>
</td>
) : (
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
<Button size="sm" fill="text" variant="secondary">
{getAnnotationName(annotation)}
</Button>
</td>
)}
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{idx !== 0 && <IconButton name="arrow-up" onClick={() => onMove(idx, -1)} tooltip="Move up" />}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{dashboard.annotations.list.length > 1 && idx !== dashboard.annotations.list.length - 1 ? (
<IconButton name="arrow-down" onClick={() => onMove(idx, 1)} tooltip="Move down" />
) : null}
</td>
<td role="gridcell" style={{ width: '1%' }}>
{!annotation.builtIn && (
<DeleteButton
size="sm"
onConfirm={() => onDelete(idx)}
aria-label={`Delete query with title "${annotation.name}"`}
/>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)} )}
{showEmptyListCTA && ( {showEmptyListCTA && (
<EmptyListCTA <EmptyListCTA
@ -127,3 +131,10 @@ export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => {
</VerticalGroup> </VerticalGroup>
); );
}; };
const getStyles = () => ({
table: css`
width: 100%;
overflow-x: scroll;
`,
});

View File

@ -1,10 +1,11 @@
import { css } from '@emotion/css';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { Button } from '@grafana/ui'; import { Button, useStyles2 } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { VariablesDependenciesButton } from '../inspect/VariablesDependenciesButton'; import { VariablesDependenciesButton } from '../inspect/VariablesDependenciesButton';
@ -35,6 +36,7 @@ export function VariableEditorList({
onDelete, onDelete,
onDuplicate, onDuplicate,
}: Props): ReactElement { }: Props): ReactElement {
const styles = useStyles2(getStyles);
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
if (!result.destination || !result.source) { if (!result.destination || !result.source) {
return; return;
@ -51,40 +53,42 @@ export function VariableEditorList({
{variables.length > 0 && ( {variables.length > 0 && (
<Stack direction="column" gap={4}> <Stack direction="column" gap={4}>
<table <div className={styles.tableContainer}>
className="filter-table filter-table--hover" <table
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table} className="filter-table filter-table--hover"
role="grid" aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
> role="grid"
<thead> >
<tr> <thead>
<th>Variable</th> <tr>
<th>Definition</th> <th>Variable</th>
<th colSpan={5} /> <th>Definition</th>
</tr> <th colSpan={5} />
</thead> </tr>
<DragDropContext onDragEnd={onDragEnd}> </thead>
<Droppable droppableId="variables-list" direction="vertical"> <DragDropContext onDragEnd={onDragEnd}>
{(provided) => ( <Droppable droppableId="variables-list" direction="vertical">
<tbody ref={provided.innerRef} {...provided.droppableProps}> {(provided) => (
{variables.map((variable, index) => ( <tbody ref={provided.innerRef} {...provided.droppableProps}>
<VariableEditorListRow {variables.map((variable, index) => (
index={index} <VariableEditorListRow
key={`${variable.name}-${index}`} index={index}
variable={variable} key={`${variable.name}-${index}`}
usageTree={usages} variable={variable}
usagesNetwork={usagesNetwork} usageTree={usages}
onDelete={onDelete} usagesNetwork={usagesNetwork}
onDuplicate={onDuplicate} onDelete={onDelete}
onEdit={onEdit} onDuplicate={onDuplicate}
/> onEdit={onEdit}
))} />
{provided.placeholder} ))}
</tbody> {provided.placeholder}
)} </tbody>
</Droppable> )}
</DragDropContext> </Droppable>
</table> </DragDropContext>
</table>
</div>
<Stack> <Stack>
<VariablesDependenciesButton variables={variables} /> <VariablesDependenciesButton variables={variables} />
<Button <Button
@ -130,3 +134,10 @@ function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement {
</div> </div>
); );
} }
const getStyles = () => ({
tableContainer: css`
overflow: scroll;
width: 100%;
`,
});

View File

@ -61,6 +61,10 @@ export function getStyles(theme: GrafanaTheme2) {
overflow: auto; overflow: auto;
padding: ${theme.spacing(0.75, 1)}; padding: ${theme.spacing(0.75, 1)};
width: inherit; width: inherit;
${theme.breakpoints.down('sm')} {
width: 100%;
}
`, `,
}; };
} }

View File

@ -1,7 +1,8 @@
import React, { PropsWithChildren, useMemo } from 'react'; import React, { PropsWithChildren, useMemo, useState } from 'react';
import { VariableRefresh } from '@grafana/data'; import { VariableRefresh } from '@grafana/data';
import { Field, RadioButtonGroup } from '@grafana/ui'; import { Field, RadioButtonGroup, useTheme2 } from '@grafana/ui';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
interface Props { interface Props {
onChange: (option: VariableRefresh) => void; onChange: (option: VariableRefresh) => void;
@ -14,6 +15,16 @@ const REFRESH_OPTIONS = [
]; ];
export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren<Props>) { export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren<Props>) {
const theme = useTheme2();
const [isSmallScreen, setIsSmallScreen] = useState(false);
useMediaQueryChange({
breakpoint: theme.breakpoints.values.sm,
onChange: (e) => {
setIsSmallScreen(!e.matches);
},
});
const value = useMemo( const value = useMemo(
() => REFRESH_OPTIONS.find((o) => o.value === refresh)?.value ?? REFRESH_OPTIONS[0].value, () => REFRESH_OPTIONS.find((o) => o.value === refresh)?.value ?? REFRESH_OPTIONS[0].value,
[refresh] [refresh]
@ -21,7 +32,12 @@ export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChild
return ( return (
<Field label="Refresh" description="When to update the values of this variable"> <Field label="Refresh" description="When to update the values of this variable">
<RadioButtonGroup options={REFRESH_OPTIONS} onChange={onChange} value={value} /> <RadioButtonGroup
options={REFRESH_OPTIONS}
onChange={onChange}
value={value}
size={isSmallScreen ? 'sm' : 'md'}
/>
</Field> </Field>
); );
} }