From 20b50e5f16db5d9372ce4bb76db079f6de14df2d Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 24 Apr 2025 11:27:04 -0300 Subject: [PATCH] Grafana UI: Make `DashboardPicker` focusable (#104242) --- .../src/components/Select/SelectBase.tsx | 7 +- .../grafana-ui/src/components/Select/types.ts | 2 + .../components/Select/DashboardPicker.tsx | 117 +++++++++--------- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 899a9a460d8..c9fd5253441 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -1,5 +1,5 @@ import { isArray, negate } from 'lodash'; -import { ComponentProps, useCallback, useEffect, useRef, useState } from 'react'; +import { ComponentProps, useCallback, useEffect, useRef, useState, useImperativeHandle } from 'react'; import * as React from 'react'; import { default as ReactSelect, @@ -152,16 +152,19 @@ export function SelectBase({ isValidNewOption, formatOptionLabel, hideSelectedOptions, + selectRef, ...rest }: SelectBaseProps & Rest) { const theme = useTheme2(); const styles = getSelectStyles(theme); - const reactSelectRef = useRef<{ controlRef: HTMLElement }>(null); + const reactSelectRef = useRef(null); const [closeToBottom, setCloseToBottom] = useState(false); const selectStyles = useCustomSelectStyles(theme, width); const [hasInputValue, setHasInputValue] = useState(!!inputValue); + useImperativeHandle(selectRef, () => reactSelectRef.current!, []); + // Infer the menu position for asynchronously loaded options. menuPlacement="auto" doesn't work when the menu is // automatically opened when the component is created (it happens in SegmentSelect by setting menuIsOpen={true}). // We can remove this workaround when the bug in react-select is fixed: https://github.com/JedWatson/react-select/issues/4936 diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index b5183d97ea3..e31aee53d5a 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -116,6 +116,8 @@ export interface SelectCommonProps { loadingMessage?: string; /** Disables wrapping of multi value values when closed */ noMultiValueWrap?: boolean; + /** Use a custom ref because generic component as output of React.forwardRef is not directly possible */ + selectRef?: React.Ref; } export interface SelectAsyncProps { diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index efe594ad208..dfbb2458ade 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -1,5 +1,5 @@ import debounce from 'debounce-promise'; -import { useCallback, useEffect, useState } from 'react'; +import { forwardRef, useCallback, useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { AsyncSelectProps, AsyncSelect } from '@grafana/ui'; @@ -38,72 +38,69 @@ async function findDashboards(query = '') { const getDashboards = debounce(findDashboards, 250, { leading: true }); // TODO: this component should provide a way to apply different filters to the search APIs -export const DashboardPicker = ({ - value, - onChange, - placeholder = 'Select dashboard', - noOptionsMessage = 'No dashboards found', - ...props -}: Props) => { - const [current, setCurrent] = useState>(); +export const DashboardPicker = forwardRef( + ({ value, onChange, placeholder = 'Select dashboard', noOptionsMessage = 'No dashboards found', ...props }, ref) => { + const [current, setCurrent] = useState>(); - // This is required because the async select does not match the raw uid value - // We can not use a simple Select because the dashboard search should not return *everything* - useEffect(() => { - if (!value || value === current?.value?.uid) { - return; - } + // This is required because the async select does not match the raw uid value + // We can not use a simple Select because the dashboard search should not return *everything* + useEffect(() => { + if (!value || value === current?.value?.uid) { + return; + } - (async () => { - // value was manually changed from outside or we are rendering for the first time. - // We need to fetch dashboard information. - const dto = await getDashboardAPI().getDashboardDTO(value, undefined); + (async () => { + // value was manually changed from outside or we are rendering for the first time. + // We need to fetch dashboard information. + const dto = await getDashboardAPI().getDashboardDTO(value, undefined); - if (isDashboardV2Resource(dto)) { - setCurrent({ - value: { - uid: dto.metadata.name, - title: dto.spec.title, - folderTitle: dto.metadata.annotations?.[AnnoKeyFolderTitle], - folderUid: dto.metadata.annotations?.[AnnoKeyFolder], - }, - label: formatLabel(dto.metadata.annotations?.[AnnoKeyFolder], dto.spec.title), - }); - } else { - if (dto.dashboard) { + if (isDashboardV2Resource(dto)) { setCurrent({ value: { - uid: dto.dashboard.uid, - title: dto.dashboard.title, - folderTitle: dto.meta.folderTitle, - folderUid: dto.meta.folderUid, + uid: dto.metadata.name, + title: dto.spec.title, + folderTitle: dto.metadata.annotations?.[AnnoKeyFolderTitle], + folderUid: dto.metadata.annotations?.[AnnoKeyFolder], }, - label: formatLabel(dto.meta?.folderTitle, dto.dashboard.title), + label: formatLabel(dto.metadata.annotations?.[AnnoKeyFolder], dto.spec.title), }); + } else { + if (dto.dashboard) { + setCurrent({ + value: { + uid: dto.dashboard.uid, + title: dto.dashboard.title, + folderTitle: dto.meta.folderTitle, + folderUid: dto.meta.folderUid, + }, + label: formatLabel(dto.meta?.folderTitle, dto.dashboard.title), + }); + } } - } - })(); - // we don't need to rerun this effect every time `current` changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); + })(); + // we don't need to rerun this effect every time `current` changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); - const onPicked = useCallback( - (sel: SelectableValue) => { - setCurrent(sel); - onChange?.(sel?.value); - }, - [onChange, setCurrent] - ); + const onPicked = useCallback( + (sel: SelectableValue) => { + setCurrent(sel); + onChange?.(sel?.value); + }, + [onChange, setCurrent] + ); - return ( - - ); -}; + return ( + + ); + } +);