diff --git a/.betterer.results b/.betterer.results index 398edbbc345..1863e5b5e31 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1702,15 +1702,21 @@ exports[`better eslint`] = { [0, 0, 0, "React Hook \\"useSnappingSplitter\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"] ], "public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"] ], "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"] ], "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], @@ -1884,18 +1890,26 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "9"] ], "public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"] ], "public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], - [0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], - [0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"], + [0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "6"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "7"], + [0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "8"], + [0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "9"] ], "public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"] ], "public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index 9824d0d08df..d9dfab29114 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -406,9 +406,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); // Expand layouts section - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) - .click(); + await page.getByLabel('Expand Group layout category').click(); // Select tabs layout await page.getByLabel('Tabs').click(); @@ -695,9 +693,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); // Expand layouts section - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) - .click(); + await page.getByLabel('Expand Group layout category').click(); // Select rows layout await page.getByLabel('Rows').click(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts index d483b7b8005..a7ef95b1f4d 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts @@ -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\)/); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index 6cd56e19a11..dd496f2213d 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -31,9 +31,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -64,9 +62,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -126,9 +122,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -185,9 +179,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -222,9 +214,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -279,9 +269,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); @@ -338,9 +326,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); - await dashboardPage - .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) - .click(); + await page.getByLabel('Expand Panel layout category').click(); await page.getByLabel('Auto grid').click(); diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 5765ada1e18..7891d34e68a 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -75,11 +75,9 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] // text wrapping is enabled by default on this panel. await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100); - // FIXME very bad selector to get the correct "wrap text" toggle here. - // toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks. - await page - .locator('[id="Override 13"]') - .locator(`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`) + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12')) + .getByLabel(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')) .click(); await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); @@ -95,8 +93,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); @@ -117,19 +115,19 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')) ).toBeVisible(); + const stateOverride = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('panel-options-override-11') + ); + // confirm that "State" column is hidden by default. expect(page.getByRole('row').nth(0)).not.toContainText('State'); - - // toggle the "State" column visibility and test that it appears before re-hiding it. - // FIXME this selector is utterly godawful, but there's no way to give testIds or aria-labels or anything to - // the panel editor builder. we should fix that to make e2e's easier to write for our team. - const hideStateColumnSwitch = page.locator('[id="Override 12"]').locator('label').last(); + // toggle the "State" column visibility via the override we set up in the kitchen sink panel. + const hideStateColumnSwitch = stateOverride.locator('label').last(); await hideStateColumnSwitch.click(); expect(page.getByRole('row').nth(0)).toContainText('State'); // now change the display name of the "State" column. - // FIXME it would be good to have a better selector here too. - const displayNameInput = page.locator('[id="Override 12"]').locator('input[value="State"]').last(); + const displayNameInput = stateOverride.locator('input[value="State"]').last(); await displayNameInput.fill('State (renamed)'); await displayNameInput.press('Enter'); expect(page.getByRole('row').nth(0)).toContainText('State (renamed)'); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 5b23a4fefa6..43daedcbdb6 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -37,7 +37,7 @@ export const colorPickerFactory = ( pickerTriggerRef = createRef(); 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 = ( }) ) : ( extends ColorPickerProps, PopoverContentProps { diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx index 63240a9b0eb..cce936f01f7 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx @@ -10,7 +10,7 @@ import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils'; type Props = StandardEditorProps; // 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 ( <> 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 ( void; value?: string; width?: number; + id?: string; } function formatCreateLabel(input: string) { @@ -21,7 +22,7 @@ export class UnitPicker extends PureComponent { }; render() { - const { value, width } = this.props; + const { value, width, id } = this.props; // Set the current selection let current: SelectableValue | undefined = undefined; @@ -56,6 +57,7 @@ export class UnitPicker extends PureComponent { return ( { return ( 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)
extends PureComponent, State> render() { const { options, isLoading } = this.state; - const { value, onChange, item } = this.props; + const { value, onChange, item, id } = this.props; const { settings } = item; return ( + inputId={id} isLoading={isLoading} value={value} defaultValue={value} diff --git a/public/app/core/components/OptionsUI/number.tsx b/public/app/core/components/OptionsUI/number.tsx index e7b0db58bbe..cd69f2b0370 100644 --- a/public/app/core/components/OptionsUI/number.tsx +++ b/public/app/core/components/OptionsUI/number.tsx @@ -6,7 +6,7 @@ import { NumberInput } from './NumberInput'; type Props = StandardEditorProps; -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 ( extends PureComponent, State> { 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 extends PureComponent, State> { } return ( + inputId={id} isLoading={isLoading} value={current} defaultValue={value} diff --git a/public/app/core/components/OptionsUI/slider.tsx b/public/app/core/components/OptionsUI/slider.tsx index b9bb41bbd90..01835ea9a38 100644 --- a/public/app/core/components/OptionsUI/slider.tsx +++ b/public/app/core/components/OptionsUI/slider.tsx @@ -11,7 +11,7 @@ import { NumberInput } from './NumberInput'; type Props = StandardEditorProps; -export const SliderValueEditor = ({ value, onChange, item }: Props) => { +export const SliderValueEditor = ({ value, onChange, item, id }: Props) => { // Input reference const inputRef = useRef(null); @@ -109,7 +109,7 @@ export const SliderValueEditor = ({ value, onChange, item }: Props) => { included={included} /> - +
diff --git a/public/app/core/components/OptionsUI/string.tsx b/public/app/core/components/OptionsUI/string.tsx index 8001bf096af..7ec4635f7c8 100644 --- a/public/app/core/components/OptionsUI/string.tsx +++ b/public/app/core/components/OptionsUI/string.tsx @@ -8,7 +8,7 @@ interface Props extends StandardEditorProps { 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 ( ; -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 (
- + ); } - return ; + return ; } const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx index 796987d8773..30b958a1642 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingEditor.tsx @@ -54,6 +54,7 @@ export function useConditionalRenderingEditor( }).addItem( new OptionsPaneItemDescriptor({ title, + id: 'conditional-rendering-options-item', render: () => , }) ); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 93e5ec503d8..85ac93b5657 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo, useRef } from 'react'; +import { ReactNode, useId, useMemo, useRef } from 'react'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; @@ -36,10 +36,10 @@ export class DashboardEditableElement implements EditableDashboardElement { // When layout changes we need to update options list const { body } = dashboard.useState(); + const dashboardTitleInputId = useId(); + const dashboardDescriptionInputId = useId(); const dashboardOptions = useMemo(() => { - const dashboardTitleInputId = 'dashboard-title-input'; - const dashboardDescriptionInputId = 'dashboard-description-input'; const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' }) .addItem( new OptionsPaneItemDescriptor({ @@ -57,7 +57,7 @@ export class DashboardEditableElement implements EditableDashboardElement { ); return editPaneHeaderOptions; - }, [dashboard]); + }, [dashboard, dashboardDescriptionInputId, dashboardTitleInputId]); const layoutCategory = useLayoutCategory(body); diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 574028a75d8..3138cb75348 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; @@ -42,19 +42,24 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] { const panel = this.panel; const layoutElement = panel.parent!; + const rootId = useId(); + const titleId = useId(); + const descriptionId = useId(); + const backgroundId = useId(); const panelOptions = useMemo(() => { return new OptionsPaneCategoryDescriptor({ title: '', id: 'panel-options' }) .addItem( new OptionsPaneItemDescriptor({ title: '', + id: rootId, render: () => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.title-option', 'Title'), - id: 'PanelFrameTitle', + id: titleId, value: panel.state.title, popularRank: 1, render: (descriptor) => ( @@ -65,7 +70,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.description', 'Description'), - id: 'description-text-area', + id: descriptionId, value: panel.state.description, render: (descriptor) => , }) @@ -73,11 +78,11 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.viz-panel.options.transparent-background', 'Transparent background'), - id: 'transparent-background', + id: backgroundId, render: (descriptor) => , }) ); - }, [panel, isNewElement]); + }, [rootId, titleId, panel, descriptionId, backgroundId, isNewElement]); const layoutCategories = useMemo( () => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []), @@ -133,9 +138,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc } } -type OpenPanelEditVizProps = { - panel: VizPanel; -}; +type OpenPanelEditVizProps = { panel: VizPanel }; const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => { return ( diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index 366fcdc81e4..c78b7a1a6b1 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -37,7 +37,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'), - id: 'PanelFrameTitle', + id: 'panel-frame-options-title', value: panel.state.title, popularRank: 1, render: function renderTitle(descriptor) { @@ -55,7 +55,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: 'panel-frame-options-description', value: panel.state.description, render: function renderDescription(descriptor) { return ; @@ -71,7 +71,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: 'panel-frame-options-transparent-bg', render: function renderTransparent(descriptor) { return ; }, @@ -86,6 +86,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'), + id: 'panel-frame-options-panel-links', render: () => , }) ) diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx index dc20ec7a8bd..0d7b7d8138c 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemEditor.tsx @@ -16,7 +16,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: 'auto-grid-repeat-by-variable', 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.' diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx index 76dfd63d364..fb2416f437c 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx @@ -15,6 +15,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan options.push( new OptionsPaneItemDescriptor({ + id: 'layout-auto-grid-column-options', title: t('dashboard-scene.get-edit-options.title.column-options', 'Column options'), skipField: true, render: () => , @@ -23,6 +24,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan options.push( new OptionsPaneItemDescriptor({ + id: 'layout-auto-grid-row-height', title: t('dashboard-scene.get-edit-options.title.row-height-options', 'Row height options'), skipField: true, render: () => , diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx index 43929dd09c6..d51b58d52dd 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx @@ -14,15 +14,16 @@ import { dashboardEditActions } from '../../edit-pane/shared'; import { DashboardGridItem } from './DashboardGridItem'; export function getDashboardGridItemOptions(gridItem: DashboardGridItem): OptionsPaneCategoryDescriptor[] { + const categoryId = 'repeat-options'; const repeatCategory = new OptionsPaneCategoryDescriptor({ title: t('dashboard.default-layout.item-options.repeat.title', 'Repeat options'), - id: 'Repeat options', + id: categoryId, isOpenDefault: false, }) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'), - id: 'repeat-by-variable-select', + id: `${categoryId}-repeat-by-variable`, 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.' @@ -33,6 +34,7 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.default-layout.item-options.repeat.direction.title', 'Repeat direction'), + id: `${categoryId}-repeat-direction`, useShowIf: () => { const { variableName } = gridItem.useState(); return Boolean(variableName); @@ -43,11 +45,12 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'), + id: `${categoryId}-max-per-row`, useShowIf: () => { const { variableName, repeatDirection } = gridItem.useState(); return Boolean(variableName) && repeatDirection === 'h'; }, - render: () => , + render: (descriptor) => , }) ); @@ -90,7 +93,7 @@ function RepeatDirectionOption({ gridItem }: OptionComponentProps) { ); } -function MaxPerRowOption({ gridItem }: OptionComponentProps) { +function MaxPerRowOption({ gridItem, id }: OptionComponentProps & { id?: string }) { const { maxPerRow } = gridItem.useState(); const maxPerRowOptions: Array> = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), @@ -99,6 +102,7 @@ function MaxPerRowOption({ gridItem }: OptionComponentProps) { return ( row.setState({ title: e.currentTarget.value })} />; + return 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 +106,7 @@ function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: Das return ( <> { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index 396e4075587..fd6ac94f5b4 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; @@ -21,10 +21,11 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa const rowCategory = useMemo( () => - new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' }) + new OptionsPaneCategoryDescriptor({ title: '', id: 'dash-row-edit' }) .addItem( new OptionsPaneItemDescriptor({ title: '', + id: 'dash-row-title', skipField: true, render: () => , }) @@ -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: () => , + id: 'dash-row-fill-screen', + render: (descriptor) => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.rows-layout.row-options.row.hide-header', 'Hide row header'), - render: () => , + id: 'dash-row-hide-header', + render: (descriptor) => , }) ), [model, isNewElement] @@ -48,16 +51,17 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa () => new OptionsPaneCategoryDescriptor({ title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'), - id: 'repeat-options', + id: 'dash-row-repeat', isOpenDefault: false, }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat by variable'), + id: `dash-row-repeat-by-variable`, description: t( 'dashboard.rows-layout.row-options.repeat.variable.description', 'Repeat this row for each value in the selected variable.' ), - render: () => , + render: (descriptor) => , }) ), [model] @@ -94,6 +98,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool } > row.onHeaderHiddenToggle()} />; + return row.onHeaderHiddenToggle()} />; } -function FillScreenSwitch({ row }: { row: RowItem }) { +function FillScreenSwitch({ row, id }: { row: RowItem; id?: string }) { const { fillScreen } = row.useState(); - return row.onChangeFillScreen(!fillScreen)} />; + return 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 ( <> row.onChangeRepeat(repeat)} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx index 4ff26a780ae..8524589b993 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx @@ -6,9 +6,11 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { RowItems } from './RowItems'; export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] { - const options = new OptionsPaneCategoryDescriptor({ title: '', id: `rows-options` }).addItem( + const categoryId = 'rows-options'; + const options = new OptionsPaneCategoryDescriptor({ title: '', id: categoryId }).addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.row.header.title', 'Row header'), + id: `${categoryId}-row-header`, render: () => , }) ); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 0a2b0b3f30e..ba88110e4a2 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -24,7 +24,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: () => , + id: 'tab-options-title', + render: (descriptor) => , }) ), [model, isNewElement] @@ -39,11 +40,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: 'tab-options-repeat-variable', description: t( 'dashboard.tabs-layout.tab-options.repeat.variable.description', 'Repeat this tab for each value in the selected variable.' ), - render: () => , + render: (descriptor) => , }) ), [model] @@ -65,7 +67,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 +81,7 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool } > tab.onChangeRepeat(repeat)} diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index 30ad47a3879..125bd380256 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -50,19 +50,20 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) { const groupLayout = new OptionsPaneCategoryDescriptor({ title: t('dashboard.layout.common.group-layout', 'Group layout'), - id: 'group-layout-category', + id: 'dash-group-layout', isOpenDefault: false, }); const gridLayout = new OptionsPaneCategoryDescriptor({ title: t('dashboard.layout.common.panel-layout', 'Panel layout'), - id: 'grid-layout-category', + id: 'dash-grid-layout', isOpenDefault: false, }); gridLayout.addItem( new OptionsPaneItemDescriptor({ title: '', + id: 'dash-grid-layout-option', skipField: true, render: () => , }) @@ -77,6 +78,7 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) { groupLayout.addItem( new OptionsPaneItemDescriptor({ title: '', + id: 'dash-group-layout-option', skipField: true, render: () => , }) diff --git a/public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx index 0495c4c3a8a..10a4dc84b95 100644 --- a/public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import { t } from '@grafana/i18n'; import { LocalValueVariable } from '@grafana/scenes'; @@ -24,16 +24,19 @@ export class LocalVariableEditableElement implements EditableDashboardElement { public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const variable = this.variable; + const localVariableCategoryId = useId(); + const localVariableId = useId(); return useMemo(() => { const category = new OptionsPaneCategoryDescriptor({ title: '', - id: 'local-variable-options', + id: localVariableCategoryId, }); category.addItem( new OptionsPaneItemDescriptor({ title: '', + id: localVariableId, skipField: true, render: () => { return ( @@ -52,6 +55,6 @@ export class LocalVariableEditableElement implements EditableDashboardElement { ); return [category]; - }, [variable]); + }, [localVariableCategoryId, localVariableId, variable]); } } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index 86450b91f92..89d53788a44 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useMemo, useRef, useState } from 'react'; +import { FormEvent, useId, useMemo, useRef, useState } from 'react'; import { VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -46,16 +46,22 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] { const variable = this.variable; + const variableOptionsCategoryId = useId(); + const variableNameId = useId(); + const labelId = useId(); + const descriptionId = useId(); + const variableHideId = useId(); if (variable instanceof LocalValueVariable) { return useLocalVariableOptions(variable); } const basicOptions = useMemo(() => { - return new OptionsPaneCategoryDescriptor({ title: '', id: 'variable-options' }) + return new OptionsPaneCategoryDescriptor({ title: '', id: variableOptionsCategoryId, isOpenDefault: true }) .addItem( new OptionsPaneItemDescriptor({ title: '', + id: variableNameId, skipField: true, render: () => , }) @@ -63,24 +69,27 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.variable.label', 'Label'), + id: labelId, description: t('dashboard.edit-pane.variable.label-description', 'Optional display name'), - render: () => , + render: (descriptor) => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.edit-pane.variable.description', 'Description'), - render: () => , + id: descriptionId, + render: (descriptor) => , }) ) .addItem( new OptionsPaneItemDescriptor({ title: '', + id: variableHideId, skipField: true, render: () => , }) ); - }, [variable, isNewElement]); + }, [variableOptionsCategoryId, variableNameId, labelId, descriptionId, variableHideId, variable, isNewElement]); const categories = [basicOptions]; const typeCategory = useVariableTypeCategory(variable); @@ -117,12 +126,14 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc interface VariableInputProps { variable: SceneVariable; + id?: string; } function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) { const { name } = variable.useState(); const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement }); const [nameError, setNameError] = useState(); + const id = useId(); const onChange = (e: FormEvent) => { const result = validateVariableName(variable, e.currentTarget.value); @@ -138,6 +149,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable return ( { @@ -171,12 +183,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 ( { oldLabel.current = label ?? ''; @@ -201,13 +214,13 @@ function VariableLabelInput({ variable }: VariableInputProps) { ); } -function VariableDescriptionTextArea({ variable }: VariableInputProps) { +function VariableDescriptionTextArea({ variable, id }: VariableInputProps) { const { description } = variable.useState(); const oldDescription = useRef(description ?? ''); return (