Accessibility: Ensure dashboard edit panel inputs have accessible labels (#109546)

* update instantiations of OptionsPaneItemDescriptor to pass IDs - obvious changes

* update instantiations of OptionsPaneItemDescriptor to pass IDs - iffy changes

* update editors to pass ID through or note a missing label

* update playwright tests

* fix unit tests

* grafana ui components updated to pass ID through

* update components to pass ID through

* add missing input IDs

* better default ID handling

* remove TS note

* revert accidental non-html id change

* kick CI

* fix old-arch e2e tests

* change to plain useId calls

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Luminessa Starlight 2025-08-14 14:01:25 -04:00 committed by GitHub
parent 37b0a49027
commit 4d067059c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 369 additions and 166 deletions

View File

@ -34,7 +34,7 @@ test.describe(
const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/);
await page.locator('#transparent-background').click({ force: true });
await page.getByRole('switch', { name: 'Transparent background' }).click({ force: true });
const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/);

View File

@ -95,8 +95,8 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Cell options Cell value inspect'))
.first()
.locator('label[for="custom.inspect"]')
.click();
.getByRole('switch', { name: 'Cell value inspect' })
.click({ force: true });
await loremIpsumCell.hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);

View File

@ -17,35 +17,35 @@ describe('Geomap layer controls options', () => {
e2e.components.PanelEditor.showZoomField()
.should('be.visible')
.within(() => {
cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked');
cy.get('input[type="checkbox"]').check({ force: true });
});
// Show attribution
e2e.components.PanelEditor.showAttributionField()
.should('be.visible')
.within(() => {
cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked');
cy.get('input[type="checkbox"]').check({ force: true });
});
// Show scale
e2e.components.PanelEditor.showScaleField()
.should('be.visible')
.within(() => {
cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked');
cy.get('input[type="checkbox"]').check({ force: true });
});
// Show measure tool
e2e.components.PanelEditor.showMeasureField()
.should('be.visible')
.within(() => {
cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked');
cy.get('input[type="checkbox"]').check({ force: true });
});
// Show debug
e2e.components.PanelEditor.showDebugField()
.should('be.visible')
.within(() => {
cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked');
cy.get('input[type="checkbox"]').check({ force: true });
});
e2e.components.Panels.Panel.content({ timeout: TIMEOUT })

View File

@ -37,7 +37,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
pickerTriggerRef = createRef<any>();
render() {
const { theme, children, onChange, color } = this.props;
const { theme, children, onChange, color, id } = this.props;
const styles = getStyles(theme);
const popoverElement = React.createElement(popover, {
...{ ...this.props, children: null },
@ -67,6 +67,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
})
) : (
<ColorSwatch
id={id}
ref={this.pickerTriggerRef}
onClick={showPopper}
onMouseLeave={hidePopper}

View File

@ -22,6 +22,7 @@ export interface ColorPickerProps extends Themeable2 {
color: string;
onChange: ColorPickerChangeHandler;
enableNamedColors?: boolean;
id?: string;
}
export interface Props<T> extends ColorPickerProps, PopoverContentProps {

View File

@ -10,7 +10,7 @@ import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils';
type Props = StandardEditorProps<string, FieldNamePickerConfigSettings>;
// Pick a field name out of the fields
export const FieldNamePicker = ({ value, onChange, context, item }: Props) => {
export const FieldNamePicker = ({ value, onChange, context, item, id }: Props) => {
const settings: FieldNamePickerConfigSettings = item.settings ?? {};
const names = useFieldDisplayNames(context.data, settings?.filter);
const selectOptions = useSelectOptions(names, value, undefined, undefined, settings.baseNameMode);
@ -29,6 +29,7 @@ export const FieldNamePicker = ({ value, onChange, context, item }: Props) => {
return (
<>
<Select
inputId={id}
value={selectedOption}
placeholder={
settings.placeholderText ?? t('grafana-ui.matchers-ui.field-name-picker.placeholder', 'Select field')

View File

@ -41,10 +41,11 @@ export interface Props {
data: DataFrame[];
onChange: (value: string) => void;
placeholder?: string;
id?: string;
}
// Not exported globally... but used in grafana core
export function RefIDPicker({ value, data, onChange, placeholder }: Props) {
export function RefIDPicker({ value, data, onChange, placeholder, id }: Props) {
const listOfRefIds = useMemo(() => getListOfQueryRefIds(data), [data]);
const [priorSelectionState, updatePriorSelectionState] = useState<{
@ -77,6 +78,7 @@ export function RefIDPicker({ value, data, onChange, placeholder }: Props) {
}
return (
<Select
inputId={id}
options={listOfRefIds}
onChange={onFilterChange}
isClearable={true}
@ -114,9 +116,10 @@ export interface MultiProps {
data: DataFrame[];
onChange: (value: string[]) => void;
placeholder?: string;
id?: string;
}
export function RefIDMultiPicker({ value, data, onChange, placeholder }: MultiProps) {
export function RefIDMultiPicker({ value, data, onChange, placeholder, id }: MultiProps) {
const listOfRefIds = useMemo(() => getListOfQueryRefIds(data), [data]);
const [priorSelectionState, updatePriorSelectionState] = useState<{
@ -172,6 +175,7 @@ export function RefIDMultiPicker({ value, data, onChange, placeholder }: MultiPr
}
return (
<MultiSelect
inputId={id}
options={listOfRefIds}
onChange={onFilterChange}
isClearable={true}

View File

@ -9,6 +9,7 @@ export interface UnitPickerProps {
onChange: (item?: string) => void;
value?: string;
width?: number;
id?: string;
}
function formatCreateLabel(input: string) {
@ -21,7 +22,7 @@ export class UnitPicker extends PureComponent<UnitPickerProps> {
};
render() {
const { value, width } = this.props;
const { value, width, id } = this.props;
// Set the current selection
let current: SelectableValue<string> | undefined = undefined;
@ -56,6 +57,7 @@ export class UnitPicker extends PureComponent<UnitPickerProps> {
return (
<Cascader
id={id}
width={width}
initialValue={current && current.label}
allowCustomValue

View File

@ -44,6 +44,9 @@ export interface NestedFolderPickerProps {
/* Whether the picker should be clearable */
clearable?: boolean;
/* HTML ID for the button element for form labels */
id?: string;
}
const debouncedSearch = debounce(getSearchResults, 300);
@ -68,6 +71,7 @@ export function NestedFolderPicker({
excludeUIDs,
permission = 'edit',
onChange,
id,
}: NestedFolderPickerProps) {
const styles = useStyles2(getStyles);
const selectedFolder = useGetFolderQueryFacade(value);
@ -283,6 +287,7 @@ export function NestedFolderPicker({
if (!overlayOpen) {
return (
<Trigger
id={id}
label={labelComponent}
handleClearSelection={clearable && value !== undefined ? handleClearSelection : undefined}
invalid={invalid}

View File

@ -5,6 +5,7 @@ import * as React from 'react';
import { Field, Input } from '@grafana/ui';
interface Props {
id?: string;
value?: number;
placeholder?: string;
autoFocus?: boolean;
@ -101,6 +102,7 @@ export class NumberInput extends PureComponent<Props, State> {
return (
<Input
type="number"
id={this.props.id}
ref={this.inputRef}
min={this.props.min}
max={this.props.max}

View File

@ -14,6 +14,7 @@ export interface ColorValueEditorSettings {
}
interface Props {
id?: string;
value?: string;
onChange: (value: string | undefined) => void;
settings?: ColorValueEditorSettings;
@ -25,7 +26,7 @@ interface Props {
/**
* @alpha
* */
export const ColorValueEditor = ({ value, settings, onChange, details }: Props) => {
export const ColorValueEditor = ({ value, settings, onChange, details, id }: Props) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
@ -37,6 +38,7 @@ export const ColorValueEditor = ({ value, settings, onChange, details }: Props)
<div className={styles.colorPicker}>
<ColorSwatch
ref={ref}
id={id}
onClick={showColorPicker}
onMouseLeave={hideColorPicker}
color={value ? theme.visualization.getColorByName(value) : theme.components.input.borderColor}

View File

@ -55,11 +55,12 @@ export class MultiSelectValueEditor<T> extends PureComponent<Props<T>, State<T>>
render() {
const { options, isLoading } = this.state;
const { value, onChange, item } = this.props;
const { value, onChange, item, id } = this.props;
const { settings } = item;
return (
<MultiSelect<T>
inputId={id}
isLoading={isLoading}
value={value}
defaultValue={value}

View File

@ -6,7 +6,7 @@ import { NumberInput } from './NumberInput';
type Props = StandardEditorProps<number, NumberFieldConfigSettings>;
export const NumberValueEditor = ({ value, onChange, item }: Props) => {
export const NumberValueEditor = ({ value, onChange, item, id }: Props) => {
const { settings } = item;
const onValueChange = useCallback(
@ -18,6 +18,7 @@ export const NumberValueEditor = ({ value, onChange, item }: Props) => {
return (
<NumberInput
id={id}
value={value}
min={settings?.min}
max={settings?.max}

View File

@ -51,7 +51,7 @@ export class SelectValueEditor<T> extends PureComponent<Props<T>, State<T>> {
render() {
const { options, isLoading } = this.state;
const { value, onChange, item } = this.props;
const { value, onChange, item, id } = this.props;
const { settings } = item;
let current = options.find((v) => v.value === value);
@ -63,6 +63,7 @@ export class SelectValueEditor<T> extends PureComponent<Props<T>, State<T>> {
}
return (
<Select<T>
inputId={id}
isLoading={isLoading}
value={current}
defaultValue={value}

View File

@ -11,7 +11,7 @@ import { NumberInput } from './NumberInput';
type Props = StandardEditorProps<number, SliderFieldConfigSettings>;
export const SliderValueEditor = ({ value, onChange, item }: Props) => {
export const SliderValueEditor = ({ value, onChange, item, id }: Props) => {
// Input reference
const inputRef = useRef<HTMLSpanElement>(null);
@ -109,7 +109,7 @@ export const SliderValueEditor = ({ value, onChange, item }: Props) => {
included={included}
/>
<span className={stylesSlider.numberInputWrapper} ref={inputRef}>
<NumberInput value={sliderValue} onChange={onSliderInputChange} max={max} min={min} step={step} />
<NumberInput id={id} value={sliderValue} onChange={onSliderInputChange} max={max} min={min} step={step} />
</span>
</div>
</div>

View File

@ -8,7 +8,7 @@ interface Props extends StandardEditorProps<string, StringFieldConfigSettings> {
suffix?: ReactNode;
}
export const StringValueEditor = ({ value, onChange, item, suffix }: Props) => {
export const StringValueEditor = ({ value, onChange, item, suffix, id }: Props) => {
const Component = item.settings?.useTextarea ? TextArea : Input;
const onValueChange = useCallback(
(
@ -36,6 +36,7 @@ export const StringValueEditor = ({ value, onChange, item, suffix }: Props) => {
return (
<Component
id={id}
placeholder={item.settings?.placeholder}
defaultValue={value || ''}
rows={(item.settings?.useTextarea && item.settings.rows) || 5}

View File

@ -6,14 +6,14 @@ import { IconButton, UnitPicker, useStyles2 } from '@grafana/ui';
type Props = StandardEditorProps<string, UnitFieldConfigSettings>;
export function UnitValueEditor({ value, onChange, item }: Props) {
export function UnitValueEditor({ value, onChange, item, id }: Props) {
const styles = useStyles2(getStyles);
if (item?.settings?.isClearable && value != null) {
return (
<div className={styles.wrapper}>
<span className={styles.first}>
<UnitPicker value={value} onChange={onChange} />
<UnitPicker value={value} onChange={onChange} id={id} />
</span>
<IconButton
name="times"
@ -23,7 +23,7 @@ export function UnitValueEditor({ value, onChange, item }: Props) {
</div>
);
}
return <UnitPicker value={value} onChange={onChange} />;
return <UnitPicker value={value} onChange={onChange} id={id} />;
}
const getStyles = (theme: GrafanaTheme2) => ({

View File

@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { t } from '@grafana/i18n';
import { Icon, Stack, Tooltip } from '@grafana/ui';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
@ -29,6 +31,7 @@ export function useConditionalRenderingEditor(
}).addItem(
new OptionsPaneItemDescriptor({
title,
id: uuidv4(),
render: () => <conditionalRendering.Component model={conditionalRendering} />,
})
);

View File

@ -1,4 +1,5 @@
import { ReactNode, useMemo, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Trans, t } from '@grafana/i18n';
import { SceneObject } from '@grafana/scenes';
@ -38,8 +39,8 @@ export class DashboardEditableElement implements EditableDashboardElement {
const { body } = dashboard.useState();
const dashboardOptions = useMemo(() => {
const dashboardTitleInputId = 'dashboard-title-input';
const dashboardDescriptionInputId = 'dashboard-description-input';
const dashboardTitleInputId = uuidv4();
const dashboardDescriptionInputId = uuidv4();
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' })
.addItem(
new OptionsPaneItemDescriptor({

View File

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Trans, t } from '@grafana/i18n';
import { locationService } from '@grafana/runtime';
@ -48,13 +49,14 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
.addItem(
new OptionsPaneItemDescriptor({
title: '',
id: uuidv4(),
render: () => <OpenPanelEditViz panel={this.panel} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.viz-panel.options.title-option', 'Title'),
id: 'PanelFrameTitle',
id: uuidv4(),
value: panel.state.title,
popularRank: 1,
render: (descriptor) => (
@ -65,7 +67,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.viz-panel.options.description', 'Description'),
id: 'description-text-area',
id: uuidv4(),
value: panel.state.description,
render: (descriptor) => <PanelDescriptionTextArea id={descriptor.props.id} panel={panel} />,
})
@ -73,7 +75,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.viz-panel.options.transparent-background', 'Transparent background'),
id: 'transparent-background',
id: uuidv4(),
render: (descriptor) => <PanelBackgroundSwitch id={descriptor.props.id} panel={panel} />,
})
);
@ -133,9 +135,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
}
}
type OpenPanelEditVizProps = {
panel: VizPanel;
};
type OpenPanelEditVizProps = { panel: VizPanel };
const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => {
return (

View File

@ -1,4 +1,5 @@
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { CoreApp } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -37,7 +38,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'),
id: 'PanelFrameTitle',
id: uuidv4(),
value: panel.state.title,
popularRank: 1,
render: function renderTitle(descriptor) {
@ -55,7 +56,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'),
id: 'description-text-area',
id: uuidv4(),
value: panel.state.description,
render: function renderDescription(descriptor) {
return <PanelDescriptionTextArea id={descriptor.props.id} panel={panel} />;
@ -71,7 +72,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'),
id: 'transparent-background',
id: uuidv4(),
render: function renderTransparent(descriptor) {
return <PanelBackgroundSwitch id={descriptor.props.id} panel={panel} />;
},
@ -86,6 +87,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
id: uuidv4(),
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
})
)

View File

@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { t } from '@grafana/i18n';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@ -16,7 +18,7 @@ export function getOptions(model: AutoGridItem): OptionsPaneCategoryDescriptor[]
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.auto-grid.item-options.repeat.variable.title', 'Repeat by variable'),
id: 'repeat-by-variable-select',
id: uuidv4(),
description: t(
'dashboard.auto-grid.item-options.repeat.variable.description',
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.'

View File

@ -1,4 +1,5 @@
import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
@ -21,7 +22,7 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'),
id: 'repeat-by-variable-select',
id: uuidv4(),
description: t(
'dashboard.default-layout.item-options.repeat.variable.description',
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.'
@ -42,11 +43,12 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'),
id: uuidv4(),
useShowIf: () => {
const { variableName, repeatDirection } = gridItem.useState();
return Boolean(variableName) && repeatDirection === 'h';
},
render: () => <MaxPerRowOption gridItem={gridItem} />,
render: (descriptor) => <MaxPerRowOption id={descriptor.props.id} gridItem={gridItem} />,
})
);
@ -81,7 +83,7 @@ function RepeatDirectionOption({ gridItem }: OptionComponentProps) {
);
}
function MaxPerRowOption({ gridItem }: OptionComponentProps) {
function MaxPerRowOption({ gridItem, id }: OptionComponentProps & { id?: string }) {
const { maxPerRow } = gridItem.useState();
const maxPerRowOptions: Array<SelectableValue<number>> = [2, 3, 4, 6, 8, 12].map((value) => ({
label: value.toString(),
@ -90,6 +92,7 @@ function MaxPerRowOption({ gridItem }: OptionComponentProps) {
return (
<Select
id={id}
options={maxPerRowOptions}
value={maxPerRow ?? 4}
onChange={(value) => {

View File

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
@ -46,7 +47,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.default-layout.row-options.form.title', 'Title'),
render: () => <RowTitleInput row={row} />,
id: uuidv4(),
render: (descriptor) => <RowTitleInput id={descriptor.props.id} row={row} />,
})
);
}, [row]);
@ -61,7 +63,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.default-layout.row-options.repeat.variable.title', 'Variable'),
render: () => <RowRepeatSelect row={row} dashboard={dashboard} />,
id: uuidv4(),
render: (descriptor) => <RowRepeatSelect id={descriptor.props.id} row={row} dashboard={dashboard} />,
})
);
}, [row]);
@ -78,13 +81,13 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
}
}
function RowTitleInput({ row }: { row: SceneGridRow }) {
function RowTitleInput({ row, id }: { row: SceneGridRow; id?: string }) {
const { title } = row.useState();
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
return <Input id={id} value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
}
function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: DashboardScene }) {
function RowRepeatSelect({ row, dashboard, id }: { row: SceneGridRow; dashboard: DashboardScene; id?: string }) {
const { $behaviors, children } = row.useState();
let repeatBehavior = $behaviors?.find((b) => b instanceof RowRepeaterBehavior);
const vizPanels = useMemo(
@ -104,6 +107,7 @@ function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: Das
return (
<>
<RepeatRowSelect2
id={id}
sceneContext={dashboard}
repeat={repeatBehavior?.state.variableName}
onChange={(repeat) => {

View File

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useId, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
@ -32,13 +33,15 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.row.fill-screen', 'Fill screen'),
render: () => <FillScreenSwitch row={model} />,
id: uuidv4(),
render: (descriptor) => <FillScreenSwitch id={descriptor.props.id} row={model} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.row.hide-header', 'Hide row header'),
render: () => <RowHeaderSwitch row={model} />,
id: uuidv4(),
render: (descriptor) => <RowHeaderSwitch id={descriptor.props.id} row={model} />,
})
),
[model, isNewElement]
@ -53,11 +56,12 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat by variable'),
id: uuidv4(),
description: t(
'dashboard.rows-layout.row-options.repeat.variable.description',
'Repeat this row for each value in the selected variable.'
),
render: () => <RowRepeatSelect row={model} />,
render: (descriptor) => <RowRepeatSelect id={descriptor.props.id} row={model} />,
})
),
[model]
@ -94,6 +98,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
}
>
<Input
id={useId()}
ref={ref}
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
value={title}
@ -103,19 +108,19 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
);
}
function RowHeaderSwitch({ row }: { row: RowItem }) {
function RowHeaderSwitch({ row, id }: { row: RowItem; id?: string }) {
const { hideHeader: isHeaderHidden = false } = row.useState();
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
return <Switch id={id} value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
}
function FillScreenSwitch({ row }: { row: RowItem }) {
function FillScreenSwitch({ row, id }: { row: RowItem; id?: string }) {
const { fillScreen } = row.useState();
return <Switch value={fillScreen} onChange={() => row.onChangeFillScreen(!fillScreen)} />;
return <Switch id={id} value={fillScreen} onChange={() => row.onChangeFillScreen(!fillScreen)} />;
}
function RowRepeatSelect({ row }: { row: RowItem }) {
function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) {
const { layout } = row.useState();
const dashboard = useDashboard(row);
@ -131,6 +136,7 @@ function RowRepeatSelect({ row }: { row: RowItem }) {
return (
<>
<RepeatRowSelect2
id={id}
sceneContext={dashboard}
repeat={row.state.repeatByVariable}
onChange={(repeat) => row.onChangeRepeat(repeat)}

View File

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
@ -24,7 +25,8 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
render: () => <TabTitleInput tab={model} isNewElement={isNewElement} />,
id: uuidv4(),
render: (descriptor) => <TabTitleInput id={descriptor.props.id} tab={model} isNewElement={isNewElement} />,
})
),
[model, isNewElement]
@ -39,11 +41,12 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.tabs-layout.tab-options.repeat.variable.title', 'Repeat by variable'),
id: uuidv4(),
description: t(
'dashboard.tabs-layout.tab-options.repeat.variable.description',
'Repeat this tab for each value in the selected variable.'
),
render: () => <TabRepeatSelect tab={model} />,
render: (descriptor) => <TabRepeatSelect id={descriptor.props.id} tab={model} />,
})
),
[model]
@ -65,7 +68,7 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
return editOptions;
}
function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: boolean }) {
function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: boolean; id?: string }) {
const { title } = tab.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
@ -79,6 +82,7 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool
}
>
<Input
id={id}
ref={ref}
title={t('dashboard.tabs-layout.tab-options.title-option', 'Title')}
value={title}
@ -88,7 +92,7 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool
);
}
function TabRepeatSelect({ tab }: { tab: TabItem }) {
function TabRepeatSelect({ tab, id }: { tab: TabItem; id?: string }) {
const { layout } = tab.useState();
const dashboard = useDashboard(tab);
@ -104,6 +108,7 @@ function TabRepeatSelect({ tab }: { tab: TabItem }) {
return (
<>
<RepeatRowSelect2
id={id}
sceneContext={dashboard}
repeat={tab.state.repeatByVariable}
onChange={(repeat) => tab.onChangeRepeat(repeat)}

View File

@ -1,4 +1,5 @@
import { FormEvent, useMemo, useRef, useState } from 'react';
import { FormEvent, useId, useMemo, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { VariableHide } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -63,14 +64,16 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.label', 'Label'),
id: uuidv4(),
description: t('dashboard.edit-pane.variable.label-description', 'Optional display name'),
render: () => <VariableLabelInput variable={variable} />,
render: (descriptor) => <VariableLabelInput id={descriptor.props.id} variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.description', 'Description'),
render: () => <VariableDescriptionTextArea variable={variable} />,
id: uuidv4(),
render: (descriptor) => <VariableDescriptionTextArea id={descriptor.props.id} variable={variable} />,
})
)
.addItem(
@ -117,6 +120,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
interface VariableInputProps {
variable: SceneVariable;
id?: string;
}
function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) {
@ -138,6 +142,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
return (
<Field label={t('dashboard.edit-pane.variable.name', 'Name')} invalid={!!nameError} error={nameError}>
<Input
id={useId()}
ref={ref}
value={name}
onFocus={() => {
@ -171,12 +176,13 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
);
}
function VariableLabelInput({ variable }: VariableInputProps) {
function VariableLabelInput({ variable, id }: VariableInputProps) {
const { label } = variable.useState();
const oldLabel = useRef(label ?? '');
return (
<Input
id={id}
value={label}
onFocus={() => {
oldLabel.current = label ?? '';
@ -201,13 +207,13 @@ function VariableLabelInput({ variable }: VariableInputProps) {
);
}
function VariableDescriptionTextArea({ variable }: VariableInputProps) {
function VariableDescriptionTextArea({ variable, id }: VariableInputProps) {
const { description } = variable.useState();
const oldDescription = useRef(description ?? '');
return (
<TextArea
id="description-text-area"
id={id}
value={description ?? ''}
placeholder={t('dashboard.edit-pane.variable.description-placeholder', 'Descriptive text')}
onFocus={() => {

View File

@ -1,5 +1,6 @@
import { FormEvent } from 'react';
import { lastValueFrom } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
@ -80,12 +81,13 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
return [
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
render: () => <ValuesTextField variable={variable} />,
id: uuidv4(),
render: (descriptor) => <ValuesTextField id={descriptor.props.id} variable={variable} />,
}),
];
}
function ValuesTextField({ variable }: { variable: CustomVariable }) {
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
const { query } = variable.useState();
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
@ -95,6 +97,7 @@ function ValuesTextField({ variable }: { variable: CustomVariable }) {
return (
<TextArea
id={id}
rows={2}
defaultValue={query}
onBlur={onBlur}

View File

@ -1,5 +1,6 @@
import { FormEvent } from 'react';
import { lastValueFrom } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { t } from '@grafana/i18n';
import { ConstantVariable, SceneVariable } from '@grafana/scenes';
@ -31,12 +32,13 @@ export function getConstantVariableOptions(variable: SceneVariable): OptionsPane
return [
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.constant-variable-form.label-value', 'Value'),
render: () => <ConstantValueInput variable={variable} />,
id: uuidv4(),
render: (descriptor) => <ConstantValueInput id={descriptor.props.id} variable={variable} />,
}),
];
}
function ConstantValueInput({ variable }: { variable: ConstantVariable }) {
function ConstantValueInput({ variable, id }: { variable: ConstantVariable; id?: string }) {
const { value } = variable.useState();
const onBlur = async (event: FormEvent<HTMLInputElement>) => {
@ -46,6 +48,7 @@ function ConstantValueInput({ variable }: { variable: ConstantVariable }) {
return (
<Input
id={id}
defaultValue={value.toString()}
onBlur={onBlur}
placeholder={t('dashboard-scene.constant-variable-form.placeholder-your-metric-prefix', 'Your metric prefix')}

View File

@ -1,5 +1,6 @@
import { FormEvent } from 'react';
import { lastValueFrom } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
@ -58,12 +59,13 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
return [
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
render: () => <ValuesTextField variable={variable} />,
id: uuidv4(),
render: ({ props }) => <ValuesTextField id={props.id} variable={variable} />,
}),
];
}
function ValuesTextField({ variable }: { variable: CustomVariable }) {
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
const { query } = variable.useState();
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
@ -73,6 +75,7 @@ function ValuesTextField({ variable }: { variable: CustomVariable }) {
return (
<TextArea
id={id}
rows={2}
defaultValue={query}
onBlur={onBlur}

View File

@ -1,5 +1,6 @@
import React, { FormEvent } from 'react';
import { lastValueFrom } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -80,20 +81,27 @@ export function getDataSourceVariableOptions(variable: SceneVariable): OptionsPa
return [
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.datasource-options.type', 'Type'),
render: () => <DataSourceTypeSelect variable={variable} />,
id: uuidv4(),
render: ({ props }) => <DataSourceTypeSelect id={props.id} variable={variable} />,
}),
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.datasource-options.name-filter', 'Name filter'),
id: uuidv4(),
description: t(
'dashboard.edit-pane.variable.datasource-options.name-filter-description',
'Regex filter for which data source instances to include. Leave empty for all.'
),
render: () => <DataSourceNameFilter variable={variable} />,
render: ({ props }) => <DataSourceNameFilter id={props.id} variable={variable} />,
}),
];
}
function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
interface InputProps {
variable: DataSourceVariable;
id?: string;
}
function DataSourceTypeSelect({ variable, id }: InputProps) {
const { pluginId } = variable.useState();
const options = getOptionDataSourceTypes();
@ -104,6 +112,7 @@ function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
return (
<Combobox
id={id}
options={options}
value={pluginId}
onChange={onChange}
@ -113,7 +122,7 @@ function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
);
}
function DataSourceNameFilter({ variable }: { variable: DataSourceVariable }) {
function DataSourceNameFilter({ variable, id }: InputProps) {
const { regex } = variable.useState();
const onBlur = async (evt: React.FormEvent<HTMLInputElement>) => {
@ -123,6 +132,7 @@ function DataSourceNameFilter({ variable }: { variable: DataSourceVariable }) {
return (
<Input
id={id}
defaultValue={regex}
onBlur={onBlur}
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter}

View File

@ -9,7 +9,6 @@ export function getSystemVariableOptions(variable: SceneVariable): OptionsPaneIt
return [
new OptionsPaneItemDescriptor({
title: '',
render: () => {
return (
<Stack direction="column">

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { t } from '@grafana/i18n';
import { MultiValueVariable, SceneVariableValueChangedEvent } from '@grafana/scenes';
@ -16,22 +17,25 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.selection-options.multi-value', 'Multi-value'),
render: () => <MultiValueSwitch variable={variable} />,
id: uuidv4(),
render: (descriptor) => <MultiValueSwitch id={descriptor.props.id} variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.selection-options.include-all', 'Include All value'),
id: uuidv4(),
description: t(
'dashboard.edit-pane.variable.selection-options.include-all-description',
'Enables a single option that represent all values'
),
render: () => <IncludeAllSwitch variable={variable} />,
render: (descriptor) => <IncludeAllSwitch id={descriptor.props.id} variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.selection-options.custom-all-value', 'Custom all value'),
id: uuidv4(),
description: t(
'dashboard.edit-pane.variable.selection-options.custom-all-value-description',
'A wildcard regex or other value to represent All'
@ -39,46 +43,61 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
useShowIf: () => {
return variable.useState().includeAll ?? false;
},
render: () => <CustomAllValueInput variable={variable} />,
render: (descriptor) => <CustomAllValueInput id={descriptor.props.id} variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.variable.selection-options.allow-custom-values', 'Allow custom values'),
id: uuidv4(),
description: t(
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
'Enables users to enter values'
),
render: () => <AllowCustomSwitch variable={variable} />,
render: (descriptor) => <AllowCustomSwitch id={descriptor.props.id} variable={variable} />,
})
);
}, [variable]);
}
function MultiValueSwitch({ variable }: { variable: MultiValueVariable }) {
interface InputProps {
variable: MultiValueVariable;
id?: string;
}
function MultiValueSwitch({ variable, id }: InputProps) {
const { isMulti } = variable.useState();
return <Switch value={isMulti} onChange={(evt) => variable.setState({ isMulti: evt.currentTarget.checked })} />;
return (
<Switch id={id} value={isMulti} onChange={(evt) => variable.setState({ isMulti: evt.currentTarget.checked })} />
);
}
function IncludeAllSwitch({ variable }: { variable: MultiValueVariable }) {
function IncludeAllSwitch({ variable, id }: InputProps) {
const { includeAll } = variable.useState();
return <Switch value={includeAll} onChange={(evt) => variable.setState({ includeAll: evt.currentTarget.checked })} />;
return (
<Switch
id={id}
value={includeAll}
onChange={(evt) => variable.setState({ includeAll: evt.currentTarget.checked })}
/>
);
}
function AllowCustomSwitch({ variable }: { variable: MultiValueVariable }) {
function AllowCustomSwitch({ variable, id }: InputProps) {
const { allowCustomValue } = variable.useState();
return (
<Switch
id={id}
value={allowCustomValue}
onChange={(evt) => variable.setState({ allowCustomValue: evt.currentTarget.checked })}
/>
);
}
function CustomAllValueInput({ variable }: { variable: MultiValueVariable }) {
function CustomAllValueInput({ variable, id }: InputProps) {
const { allValue } = variable.useState();
const ref = useRef<HTMLInputElement>(null);
@ -97,5 +116,5 @@ function CustomAllValueInput({ variable }: { variable: MultiValueVariable }) {
[variable]
);
return <Input ref={ref} defaultValue={allValue ?? ''} onBlur={onInputBlur} />;
return <Input id={id} ref={ref} defaultValue={allValue ?? ''} onBlur={onInputBlur} />;
}

View File

@ -24,6 +24,7 @@ export interface OptionsPaneItemInfo {
useShowIf?: () => boolean;
overrides?: OptionPaneItemOverrideInfo[];
addon?: ReactNode;
/** Must be unique on the page! */
id?: string;
}
@ -35,10 +36,7 @@ export class OptionsPaneItemDescriptor {
props: OptionsPaneItemInfo;
constructor(props: OptionsPaneItemInfo) {
this.props = { ...props, id: props.id ?? props.title };
if (this.props.id === '') {
this.props.id = uniqueId();
}
this.props = { ...props, id: props.id || uniqueId() };
}
render(searchQuery?: string) {

View File

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { cloneDeep } from 'lodash';
import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import {
FieldConfigOptionsRegistry,
@ -59,13 +60,7 @@ export function getFieldOverrideCategories(
...currentFieldConfig,
overrides: [
...currentFieldConfig.overrides,
{
matcher: {
id: info.id,
options: info.defaultOptions,
},
properties: [],
},
{ matcher: { id: info.id, options: info.defaultOptions }, properties: [] },
],
});
};
@ -109,18 +104,12 @@ export function getFieldOverrideCategories(
});
const onMatcherConfigChange = (options: unknown) => {
onOverrideChange(idx, {
...override,
matcher: { ...override.matcher, options },
});
onOverrideChange(idx, { ...override, matcher: { ...override.matcher, options } });
};
const onDynamicConfigValueAdd = (override: ConfigOverrideRule, value: SelectableValue<string>) => {
const registryItem = registry.get(value.value!);
const propertyConfig: DynamicConfigValue = {
id: registryItem.id,
value: registryItem.defaultValue,
};
const propertyConfig: DynamicConfigValue = { id: registryItem.id, value: registryItem.defaultValue };
const properties = override.properties ?? [];
properties.push(propertyConfig);
@ -131,13 +120,15 @@ export function getFieldOverrideCategories(
/**
* Add override matcher UI element
*/
const htmlId = uuidv4();
category.addItem(
new OptionsPaneItemDescriptor({
id: htmlId,
title: matcherUi.name,
render: function renderMatcherUI() {
return (
<matcherUi.component
id={`${matcherUi.matcher.id}-${idx}`}
id={htmlId}
matcher={matcherUi.matcher}
data={data ?? []}
options={override.matcher.options}
@ -173,10 +164,7 @@ export function getFieldOverrideCategories(
};
const onPropertyRemove = () => {
onOverrideChange(idx, {
...override,
properties: override.properties.filter((_, i) => i !== propIdx),
});
onOverrideChange(idx, { ...override, properties: override.properties.filter((_, i) => i !== propIdx) });
};
/**
@ -184,7 +172,6 @@ export function getFieldOverrideCategories(
*/
category.addItem(
new OptionsPaneItemDescriptor({
title: registryItemForProperty.name,
skipField: true,
render: function renderPropertyEditor() {
return (
@ -210,7 +197,6 @@ export function getFieldOverrideCategories(
if (!isSystemOverride && override.matcher.options) {
category.addItem(
new OptionsPaneItemDescriptor({
title: '----------',
skipField: true,
render: function renderAddPropertyButton() {
return (
@ -274,11 +260,7 @@ function getOverrideProperties(registry: FieldConfigOptionsRegistry) {
if (item.category) {
label = [...item.category, item.name].join(' > ');
}
return {
label,
value: item.id,
description: item.description,
};
return { label, value: item.id, description: item.description };
});
}
@ -288,9 +270,5 @@ function AddOverrideButtonContainer({ children }: { children: React.ReactNode })
}
function getBorderTopStyles(theme: GrafanaTheme2) {
return css({
borderTop: `1px solid ${theme.colors.border.weak}`,
padding: `${theme.spacing(2)}`,
display: 'flex',
});
return css({ borderTop: `1px solid ${theme.colors.border.weak}`, padding: `${theme.spacing(2)}`, display: 'flex' });
}

View File

@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { t } from '@grafana/i18n';
import { Input } from '@grafana/ui';
import { LibraryPanelInformation } from 'app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo';
@ -24,12 +26,13 @@ export function getLibraryPanelOptionsCategory(props: OptionPaneRenderProps): Op
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-library-panel-options-category.title.name', 'Name'),
id: uuidv4(),
value: panel.libraryPanel.name,
popularRank: 1,
render: function renderName() {
render: function renderName(descriptor) {
return (
<Input
id="LibraryPanelFrameName"
id={descriptor.props.id}
defaultValue={panel.libraryPanel.name}
onBlur={(e) =>
onPanelConfigChange('libraryPanel', { ...panel.libraryPanel, name: e.currentTarget.value })
@ -42,6 +45,7 @@ export function getLibraryPanelOptionsCategory(props: OptionPaneRenderProps): Op
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-library-panel-options-category.title.information', 'Information'),
id: uuidv4(),
render: function renderLibraryPanelInformation() {
return <LibraryPanelInformation panel={panel} formatDate={dashboard.formatDate} />;
},

View File

@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
@ -20,8 +22,11 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
isOpenDefault: true,
});
const panelFrameTitleId = uuidv4();
const descriptionId = uuidv4();
const setPanelTitle = (title: string) => {
const input = document.getElementById('PanelFrameTitle');
const input = document.getElementById(panelFrameTitleId);
if (input instanceof HTMLInputElement) {
input.value = title;
onPanelConfigChange('title', title);
@ -29,7 +34,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
};
const setPanelDescription = (description: string) => {
const input = document.getElementById('description-text-area');
const input = document.getElementById(descriptionId);
if (input instanceof HTMLTextAreaElement) {
input.value = description;
onPanelConfigChange('description', description);
@ -40,7 +45,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.title', 'Title'),
id: 'PanelFrameTitle',
id: panelFrameTitleId,
value: panel.title,
popularRank: 1,
render: function renderTitle(descriptor) {
@ -65,7 +70,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.description', 'Description'),
id: 'description-text-area',
id: descriptionId,
description: panel.description,
value: panel.description,
render: function renderDescription(descriptor) {
@ -86,7 +91,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.transparent-background', 'Transparent background'),
id: 'transparent-background',
id: uuidv4(),
render: function renderTransparent(descriptor) {
return (
<Switch
@ -108,6 +113,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.panel-links', 'Panel links'),
id: uuidv4(),
render: function renderLinks() {
return (
<DataLinksInlineEditor
@ -130,7 +136,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.repeat-by-variable', 'Repeat by variable'),
id: 'repeat-by-variable-select',
id: uuidv4(),
description:
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
render: function renderRepeatOptions(descriptor) {
@ -175,11 +181,13 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-panel-frame-category.title.max-per-row', 'Max per row'),
id: uuidv4(),
showIf: () => Boolean(panel.repeat && panel.repeatDirection === 'h'),
render: function renderOption() {
render: function renderOption(descriptor) {
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
return (
<Select
id={descriptor.props.id}
options={maxPerRowOptions}
value={panel.maxPerRow}
onChange={(value) => onPanelConfigChange('maxPerRow', value.value)}

View File

@ -1,4 +1,5 @@
import { get as lodashGet } from 'lodash';
import { v4 as uuiv4 } from 'uuid';
import {
EventBus,
@ -130,9 +131,12 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
category.props.itemsCount = fieldOption.getItemsCount(value);
}
const htmlId = uuiv4();
category.addItem(
new OptionsPaneItemDescriptor({
title: fieldOption.name,
id: htmlId,
description: fieldOption.description,
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
render: function renderEditor() {
@ -142,7 +146,7 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
);
};
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={fieldOption.id} />;
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={htmlId} />;
},
})
);
@ -165,12 +169,13 @@ export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryPanelBeha
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.get-library-viz-panel-options-category.title.name', 'Name'),
id: uuiv4(),
value: libraryPanel,
popularRank: 1,
render: function renderName() {
render: function renderName(descriptor) {
return (
<Input
id="LibraryPanelFrameName"
id={descriptor.props.id}
data-testid="library panel name input"
defaultValue={libraryPanel.state.name}
onBlur={(e) => libraryPanel.setState({ name: e.currentTarget.value })}
@ -264,9 +269,12 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
category.props.itemsCount = fieldOption.getItemsCount(value);
}
const htmlId = uuiv4();
category.addItem(
new OptionsPaneItemDescriptor({
title: fieldOption.name,
id: htmlId,
description: fieldOption.description,
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
render: function renderEditor() {
@ -277,7 +285,7 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
);
};
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={fieldOption.id} />;
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={htmlId} />;
},
})
);
@ -330,10 +338,13 @@ export function fillOptionsPaneItems(
continue;
}
const htmlId = uuiv4();
const Editor = pluginOption.editor;
category.addItem(
new OptionsPaneItemDescriptor({
title: pluginOption.name,
id: htmlId,
description: pluginOption.description,
render: function renderEditor() {
return (
@ -344,7 +355,7 @@ export function fillOptionsPaneItems(
}}
item={pluginOption}
context={context}
id={pluginOption.id}
id={htmlId}
/>
);
},

View File

@ -1,3 +1,5 @@
import { v4 as uuiv4 } from 'uuid';
import { OptionsPaneCategoryDescriptor } from '../OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from '../OptionsPaneItemDescriptor';
@ -51,18 +53,21 @@ function getOptionCategories(): OptionsPaneCategoryDescriptor[] {
.addItem(
new OptionsPaneItemDescriptor({
title: 'Title',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Min',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'ASDSADASDSADA',
id: uuiv4(),
description: 'DescriptionMatch',
render: jest.fn(),
})
@ -74,18 +79,21 @@ function getOptionCategories(): OptionsPaneCategoryDescriptor[] {
.addItem(
new OptionsPaneItemDescriptor({
title: 'Min',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'DescriptionMatch',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Frame',
id: uuiv4(),
render: jest.fn(),
})
),
@ -101,18 +109,21 @@ function getOverrides(): OptionsPaneCategoryDescriptor[] {
.addItem(
new OptionsPaneItemDescriptor({
title: 'Match by name',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Min',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Max',
id: uuiv4(),
render: jest.fn(),
})
),
@ -123,18 +134,21 @@ function getOverrides(): OptionsPaneCategoryDescriptor[] {
.addItem(
new OptionsPaneItemDescriptor({
title: 'Match by name',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Threshold',
id: uuiv4(),
render: jest.fn(),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Max',
id: uuiv4(),
render: jest.fn(),
})
),

View File

@ -21,7 +21,7 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo
}),
[]
);
const { value, context, onChange, item } = props;
const { value, context, onChange, item, id } = props;
const defaultColor = 'dark-green';
@ -71,6 +71,7 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo
<>
<div className={styles.container}>
<Select
inputId={id}
value={selectedOption}
options={selectOptions}
onChange={onSelectChange}

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useMemo } from 'react';
import { useCallback, useId, useMemo } from 'react';
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { t } from '@grafana/i18n';
@ -92,6 +92,8 @@ export const ScalarDimensionEditor = ({ value, context, onChange, item }: Props)
[onChange, value]
);
const valueInputId = useId();
const val = value ?? {};
const mode = value?.mode ?? ScalarDimensionMode.Mod;
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
@ -119,6 +121,7 @@ export const ScalarDimensionEditor = ({ value, context, onChange, item }: Props)
grow={true}
>
<NumberInput
id={valueInputId}
value={val?.fixed ?? DEFAULT_VALUE}
onChange={onValueChange}
max={settings?.max}

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useMemo } from 'react';
import { useCallback, useId, useMemo } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { t } from '@grafana/i18n';
@ -12,7 +12,7 @@ import { validateScaleOptions, validateScaleConfig } from '../scale';
import { ScaleDimensionOptions } from '../types';
export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionConfig, ScaleDimensionOptions>) => {
const { value, context, onChange, item } = props;
const { value, context, onChange, item, id } = props;
const { settings } = item;
const styles = useStyles2(getStyles);
@ -95,12 +95,17 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
[validateAndDoChange, value]
);
const valueInputId = useId();
const minInputId = useId();
const maxInputId = useId();
const val = value ?? {};
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
return (
<>
<div>
<Select
inputId={id}
value={selectedOption}
options={selectOptions}
onChange={onSelectChange}
@ -111,7 +116,7 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
{isFixed && (
<InlineFieldRow>
<InlineField label={t('dimensions.scale-dimension-editor.label-value', 'Value')} labelWidth={8} grow={true}>
<NumberInput value={val.fixed} {...minMaxStep} onChange={onValueChange} />
<NumberInput id={valueInputId} value={val.fixed} {...minMaxStep} onChange={onValueChange} />
</InlineField>
</InlineFieldRow>
)}
@ -119,12 +124,12 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
<>
<InlineFieldRow>
<InlineField label={t('dimensions.scale-dimension-editor.label-min', 'Min')} labelWidth={8} grow={true}>
<NumberInput value={val.min} {...minMaxStep} onChange={onMinChange} />
<NumberInput id={minInputId} value={val.min} {...minMaxStep} onChange={onMinChange} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label={t('dimensions.scale-dimension-editor.label-max', 'Max')} labelWidth={8} grow={true}>
<NumberInput value={val.max} {...minMaxStep} onChange={onMaxChange} />
<NumberInput id={maxInputId} value={val.max} {...minMaxStep} onChange={onMaxChange} />
</InlineField>
</InlineFieldRow>
</>

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useId } from 'react';
import {
FieldNamePickerConfigSettings,
@ -74,6 +74,10 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
onFixedChange('');
};
const fieldInputId = useId();
const valueInputId = useId();
const templateInputId = useId();
const mode = value?.mode ?? TextDimensionMode.Fixed;
return (
<>
@ -94,6 +98,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
grow={true}
>
<FieldNamePicker
id={fieldInputId}
context={context}
value={value.field ?? ''}
onChange={onFieldChange}
@ -110,6 +115,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
grow={true}
>
<StringValueEditor
id={valueInputId}
context={context}
value={value?.fixed}
onChange={onFixedChange}
@ -138,6 +144,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
grow={true}
>
<StringValueEditor // This could be a code editor
id={templateInputId}
context={context}
value={value?.fixed}
onChange={onFixedChange}

View File

@ -21,6 +21,7 @@ export const LocationModeEditor = ({
onChange,
context,
item,
id,
}: StandardEditorProps<string, ModeEditorSettings, unknown, unknown>) => {
const [info, setInfo] = useState<FrameGeometryField>();
@ -97,6 +98,7 @@ export const LocationModeEditor = ({
return (
<>
<Select
inputId={id}
options={MODE_OPTIONS}
value={value}
onChange={(v) => {

View File

@ -128,11 +128,12 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
description: t('alertlist.description-datasource', 'Filter from alert source'),
id: 'datasource',
defaultValue: null,
editor: function RenderDatasourcePicker(props) {
editor: function RenderDatasourcePicker({ id, ...props }) {
return (
<Stack gap={1}>
<DataSourcePicker
{...props}
inputId={id}
type={SUPPORTED_RULE_SOURCE_TYPES}
noDefault
current={props.value}

View File

@ -6,7 +6,7 @@ import { RefIDMultiPicker, RefIDPicker, stringsToRegexp } from '@grafana/ui/inte
type Props = StandardEditorProps<MatcherConfig>;
export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
export const FrameSelectionEditor = ({ value, context, onChange, id }: Props) => {
const onFilterChange = useCallback(
(v: string) => {
onChange(
@ -23,6 +23,7 @@ export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
return (
<RefIDPicker
id={id}
value={value?.options}
onChange={onFilterChange}
data={context.data}

View File

@ -1,5 +1,5 @@
import { toLonLat } from 'ol/proj';
import { useMemo, useCallback } from 'react';
import { useMemo, useCallback, useId } from 'react';
import { StandardEditorProps, SelectableValue } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
@ -63,11 +63,14 @@ export const MapViewEditor = ({
[value, onChange]
);
const viewInputId = useId();
const zoomInputId = useId();
return (
<>
<InlineFieldRow>
<InlineField label={t('geomap.map-view-editor.label-view', 'View')} labelWidth={labelWidth} grow={true}>
<Select options={views.options} value={views.current} onChange={onSelectView} />
<Select inputId={viewInputId} options={views.options} value={views.current} onChange={onSelectView} />
</InlineField>
</InlineFieldRow>
{value.id === MapCenterID.Coordinates && (
@ -88,6 +91,7 @@ export const MapViewEditor = ({
grow={true}
>
<NumberInput
id={zoomInputId}
value={value?.zoom ?? 1}
min={1}
max={18}

View File

@ -1,5 +1,5 @@
import { capitalize } from 'lodash';
import { useMemo } from 'react';
import { useId, useMemo } from 'react';
import { useObservable } from 'react-use';
import { Observable, of } from 'rxjs';
@ -121,6 +121,21 @@ export const StyleEditor = (props: Props) => {
const hasTextLabel = styleUsesText(value);
const maxFiles = 2000;
const symbolId = useId();
const rotationAngleId = useId();
const colorId = useId();
const opacityId = useId();
const sizeId = useId();
const symbol1Id = useId();
const symbolVertId = useId();
const color1Id = useId();
const fillOpacityId = useId();
const rotationAngle1Id = useId();
const textId = useId();
const fontSizeId = useId();
const xOffsetId = useId();
const yOffsetId = useId();
// Simple fixed value display
if (settings?.simpleFixedValues) {
return (
@ -130,6 +145,7 @@ export const StyleEditor = (props: Props) => {
<InlineFieldRow>
<InlineField label={t('geomap.style-editor.label-symbol', 'Symbol')}>
<ResourceDimensionEditor
id={symbolId}
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
onChange={onSymbolChange}
@ -155,6 +171,7 @@ export const StyleEditor = (props: Props) => {
</InlineFieldRow>
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
<ScalarDimensionEditor
id={rotationAngleId}
value={value?.rotation ?? defaultStyleConfig.rotation}
context={context}
onChange={onRotationChange}
@ -174,6 +191,7 @@ export const StyleEditor = (props: Props) => {
<InlineField label={t('geomap.style-editor.label-color', 'Color')} labelWidth={10}>
<InlineLabel width={4}>
<ColorPicker
id={colorId}
color={value?.color?.fixed ?? defaultStyleConfig.color.fixed}
onChange={(v) => {
onColorChange({ fixed: v });
@ -185,6 +203,7 @@ export const StyleEditor = (props: Props) => {
<InlineFieldRow>
<InlineField label={t('geomap.style-editor.label-opacity', 'Opacity')} labelWidth={10} grow>
<SliderValueEditor
id={opacityId}
value={value?.opacity ?? defaultStyleConfig.opacity}
context={context}
onChange={onOpacityChange}
@ -208,6 +227,7 @@ export const StyleEditor = (props: Props) => {
<>
<Field label={t('geomap.style-editor.label-size', 'Size')}>
<ScaleDimensionEditor
id={sizeId}
value={value?.size ?? defaultStyleConfig.size}
context={context}
onChange={onSizeChange}
@ -225,6 +245,7 @@ export const StyleEditor = (props: Props) => {
<>
<Field label={t('geomap.style-editor.label-symbol', 'Symbol')}>
<ResourceDimensionEditor
id={symbol1Id}
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
onChange={onSymbolChange}
@ -249,6 +270,7 @@ export const StyleEditor = (props: Props) => {
</Field>
<Field label={t('geomap.style-editor.label-symbol-vertical-align', 'Symbol vertical align')}>
<RadioButtonGroup
id={symbolVertId}
value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical}
onChange={onAlignVerticalChange}
options={[
@ -288,6 +310,7 @@ export const StyleEditor = (props: Props) => {
)}
<Field label={t('geomap.style-editor.label-color', 'Color')}>
<ColorDimensionEditor
id={color1Id}
value={value?.color ?? defaultStyleConfig.color}
context={context}
onChange={onColorChange}
@ -296,6 +319,7 @@ export const StyleEditor = (props: Props) => {
</Field>
<Field label={t('geomap.style-editor.label-fill-opacity', 'Fill opacity')}>
<SliderValueEditor
id={fillOpacityId}
value={value?.opacity ?? defaultStyleConfig.opacity}
context={context}
onChange={onOpacityChange}
@ -313,6 +337,7 @@ export const StyleEditor = (props: Props) => {
{settings?.displayRotation && (
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
<ScalarDimensionEditor
id={rotationAngle1Id}
value={value?.rotation ?? defaultStyleConfig.rotation}
context={context}
onChange={onRotationChange}
@ -329,6 +354,7 @@ export const StyleEditor = (props: Props) => {
)}
<Field label={t('geomap.style-editor.label-text-label', 'Text label')}>
<TextDimensionEditor
id={textId}
value={value?.text ?? defaultTextConfig}
context={context}
onChange={onTextChange}
@ -341,6 +367,7 @@ export const StyleEditor = (props: Props) => {
<HorizontalGroup>
<Field label={t('geomap.style-editor.label-font-size', 'Font size')}>
<NumberValueEditor
id={fontSizeId}
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
context={context}
onChange={onTextFontSizeChange}
@ -349,6 +376,7 @@ export const StyleEditor = (props: Props) => {
</Field>
<Field label={t('geomap.style-editor.label-x-offset', 'X offset')}>
<NumberValueEditor
id={xOffsetId}
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
context={context}
onChange={onTextOffsetXChange}
@ -357,6 +385,7 @@ export const StyleEditor = (props: Props) => {
</Field>
<Field label={t('geomap.style-editor.label-y-offset', 'Y offset')}>
<NumberValueEditor
id={yOffsetId}
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
context={context}
onChange={onTextOffsetYChange}

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { StandardEditorProps } from '@grafana/data';
import { Switch } from '@grafana/ui';
export function PaginationEditor({ onChange, value, context }: StandardEditorProps<boolean>) {
export function PaginationEditor({ onChange, value, context, id }: StandardEditorProps<boolean>) {
const changeValue = (event: React.FormEvent<HTMLInputElement> | undefined) => {
if (event?.currentTarget.checked) {
context.options.footer.show = false;
@ -11,5 +11,5 @@ export function PaginationEditor({ onChange, value, context }: StandardEditorPro
onChange(event?.currentTarget.checked);
};
return <Switch value={Boolean(value)} onChange={changeValue} />;
return <Switch value={Boolean(value)} onChange={changeValue} id={id} />;
}

View File

@ -23,9 +23,10 @@ export interface TableCellEditorProps<T> {
interface Props {
value: TableCellOptions;
onChange: (v: TableCellOptions) => void;
id?: string;
}
export const TableCellOptionEditor = ({ value, onChange }: Props) => {
export const TableCellOptionEditor = ({ value, onChange, id }: Props) => {
const cellType = value.type;
const styles = useStyles2(getStyles);
const currentMode = cellDisplayModeOptions.find((o) => o.value!.type === cellType)!;
@ -60,7 +61,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
return (
<div className={styles.fixBottomMargin}>
<Field>
<Select options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
<Select inputId={id} options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
</Field>
{(cellType === TableCellDisplayMode.Auto || cellType === TableCellDisplayMode.ColorText) && (
<AutoCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />

View File

@ -1,3 +1,5 @@
import { useId } from 'react';
import { t } from '@grafana/i18n';
import { TableAutoCellOptions, TableColorTextCellOptions } from '@grafana/schema';
import { Field, Switch } from '@grafana/ui';
@ -13,6 +15,8 @@ export const AutoCellOptionsEditor = ({
onChange(cellOptions);
};
const htmlId = useId();
return (
<Field
label={t('table.auto-cell-options-editor.label-wrap-text', 'Wrap text')}
@ -21,7 +25,7 @@ export const AutoCellOptionsEditor = ({
'If selected text will be wrapped to the width of text in the configured column'
)}
>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
<Switch id={htmlId} value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
);
};

View File

@ -1,3 +1,5 @@
import { useId } from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
@ -31,12 +33,16 @@ export const ColorBackgroundCellOptionsEditor = ({
onChange(cellOptions);
};
const applyToRowSwitchId = useId();
const wrapTextSwitchId = useId();
const label = (
<Label
description={t(
'table.color-background-cell-options-editor.description-wrap-text',
'If selected text will be wrapped to the width of text in the configured column'
)}
htmlFor={wrapTextSwitchId}
>
<Trans i18nKey="table.color-background-cell-options-editor.wrap-text">Wrap text</Trans>{' '}
<Badge
@ -65,10 +71,10 @@ export const ColorBackgroundCellOptionsEditor = ({
'If selected the entire row will be colored as this cell would be.'
)}
>
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
<Switch id={applyToRowSwitchId} value={cellOptions.applyToRow} onChange={onColorRowChange} />
</Field>
<Field label={label}>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
<Switch id={wrapTextSwitchId} value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
</>
);

View File

@ -1,4 +1,4 @@
import { FormEvent } from 'react';
import { FormEvent, useId } from 'react';
import { t } from '@grafana/i18n';
import { TableImageCellOptions } from '@grafana/schema';
@ -17,6 +17,9 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
onChange(cellOptions);
};
const altTextInputId = useId();
const titleTextInputId = useId();
return (
<>
<Field
@ -26,7 +29,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
"Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
)}
>
<Input onChange={onAltChange} defaultValue={cellOptions.alt} />
<Input id={altTextInputId} onChange={onAltChange} defaultValue={cellOptions.alt} />
</Field>
<Field
@ -36,7 +39,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
'Text that will be displayed when the image is hovered by a cursor'
)}
>
<Input onChange={onTitleChange} defaultValue={cellOptions.title} />
<Input id={titleTextInputId} onChange={onTitleChange} defaultValue={cellOptions.title} />
</Field>
</>
);

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { useId, useMemo } from 'react';
import { createFieldConfigRegistry, SetFieldConfigOptionsArgs } from '@grafana/data';
import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema';
@ -51,6 +51,8 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
const values = { ...defaultSparklineCellConfig, ...cellOptions };
const htmlIdBase = useId();
return (
<Stack direction="column" gap={0}>
{registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => {
@ -67,6 +69,7 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue}
item={item}
context={{ data: [] }}
id={`${htmlIdBase}${item.id}`}
/>
</Field>
);

View File

@ -4,7 +4,7 @@ import { StandardEditorProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Switch } from '@grafana/ui';
export function PaginationEditor({ onChange, value }: StandardEditorProps<boolean>) {
export function PaginationEditor({ onChange, value, id }: StandardEditorProps<boolean>) {
const changeValue = (event: React.FormEvent<HTMLInputElement> | undefined) => {
onChange(event?.currentTarget.checked);
};
@ -14,6 +14,7 @@ export function PaginationEditor({ onChange, value }: StandardEditorProps<boolea
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Enable pagination`)}
value={Boolean(value)}
onChange={changeValue}
id={id}
/>
);
}

View File

@ -25,6 +25,7 @@ export interface TableCellEditorProps<T> {
interface Props {
value: TableCellOptions;
onChange: (v: TableCellOptions) => void;
id?: string;
}
const TEXT_WRAP_CELL_TYPES = new Set([
@ -40,7 +41,7 @@ function isTextWrapCellType(value: TableCellOptions): value is TableCellOptions
return TEXT_WRAP_CELL_TYPES.has(value.type);
}
export const TableCellOptionEditor = ({ value, onChange }: Props) => {
export const TableCellOptionEditor = ({ value, onChange, id }: Props) => {
const cellType = value.type;
const styles = useStyles2(getStyles);
const cellDisplayModeOptions: Array<ComboboxOption<TableCellOptions['type']>> = [
@ -92,7 +93,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
return (
<div className={styles.fixBottomMargin}>
<Field>
<Combobox options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
<Combobox id={id} options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
</Field>
{isTextWrapCellType(value) && <TextWrapOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />}
{cellType === TableCellDisplayMode.Gauge && (

View File

@ -1,3 +1,5 @@
import { useId } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
@ -26,6 +28,8 @@ export const ColorBackgroundCellOptionsEditor = ({
onChange(cellOptions);
};
const htmlId = useId();
return (
<>
<Field
@ -47,6 +51,7 @@ export const ColorBackgroundCellOptionsEditor = ({
)}
>
<Switch
id={htmlId}
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Apply to entire row`)}
value={cellOptions.applyToRow}
onChange={onColorRowChange}

View File

@ -1,4 +1,4 @@
import { FormEvent } from 'react';
import { FormEvent, useId } from 'react';
import { t } from '@grafana/i18n';
import { TableImageCellOptions } from '@grafana/schema';
@ -17,6 +17,9 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
onChange(cellOptions);
};
const altTextInputId = useId();
const titleTextInputId = useId();
return (
<>
<Field
@ -26,7 +29,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
"Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
)}
>
<Input onChange={onAltChange} defaultValue={cellOptions.alt} />
<Input id={altTextInputId} onChange={onAltChange} defaultValue={cellOptions.alt} />
</Field>
<Field
@ -36,7 +39,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
'Text that will be displayed when the image is hovered by a cursor'
)}
>
<Input onChange={onTitleChange} defaultValue={cellOptions.title} />
<Input id={titleTextInputId} onChange={onTitleChange} defaultValue={cellOptions.title} />
</Field>
</>
);

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { useId, useMemo } from 'react';
import { createFieldConfigRegistry, SetFieldConfigOptionsArgs } from '@grafana/data';
import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema';
@ -51,6 +51,8 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
const values = { ...defaultSparklineCellConfig, ...cellOptions };
const htmlIdBase = useId();
return (
<VerticalGroup>
{registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => {
@ -67,6 +69,7 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue}
item={item}
context={{ data: [] }}
id={`${htmlIdBase}-${item.id}`}
/>
</Field>
);

View File

@ -1,3 +1,5 @@
import { useId } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { TableCellOptions, TableWrapTextOptions } from '@grafana/schema';
@ -15,10 +17,13 @@ export const TextWrapOptionsEditor = ({
onChange(cellOptions);
};
const htmlId = useId();
return (
<>
<Field label={t('table.text-wrap-options.label-wrap-text', 'Wrap text')}>
<Switch
id={htmlId}
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Wrap text`)}
value={cellOptions.wrapText}
onChange={onWrapTextChange}

View File

@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import { Fragment, useState } from 'react';
import { Fragment, useId, useState } from 'react';
import { usePrevious } from 'react-use';
import {
@ -77,6 +77,12 @@ export const SeriesEditor = ({
});
});
const frameInputId = useId();
const xFieldInputId = useId();
const yFieldInputId = useId();
const sizeFieldInputId = useId();
const colorFieldInputId = useId();
return (
<>
{mapping === SeriesMapping.Manual && (
@ -129,6 +135,7 @@ export const SeriesEditor = ({
<Fragment key={formKey}>
<Field label={t('xychart.series-editor.label-frame', 'Frame')}>
<Select
inputId={frameInputId}
placeholder={
mapping === SeriesMapping.Auto
? t('xychart.series-editor.placeholder-all-frames', 'All frames')
@ -158,6 +165,7 @@ export const SeriesEditor = ({
</Field>
<Field label={t('xychart.series-editor.label-x-field', 'X field')}>
<FieldNamePicker
id={xFieldInputId}
value={series.x?.matcher.options as string}
context={context}
onChange={(fieldName) => {
@ -195,6 +203,7 @@ export const SeriesEditor = ({
</Field>
<Field label={t('xychart.series-editor.label-y-field', 'Y field')}>
<FieldNamePicker
id={yFieldInputId}
value={series.y?.matcher?.options as string}
context={context}
onChange={(fieldName) => {
@ -233,6 +242,7 @@ export const SeriesEditor = ({
</Field>
<Field label={t('xychart.series-editor.label-size-field', 'Size field')}>
<FieldNamePicker
id={sizeFieldInputId}
value={series.size?.matcher?.options as string}
context={context}
onChange={(fieldName) => {
@ -268,6 +278,7 @@ export const SeriesEditor = ({
</Field>
<Field label={t('xychart.series-editor.label-color-field', 'Color field')}>
<FieldNamePicker
id={colorFieldInputId}
value={series.color?.matcher?.options as string}
context={context}
onChange={(fieldName) => {