diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 317d2b57729..3e76842a4e3 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -4,6 +4,7 @@ ignoreDeps: [ "@types/history", // this can be removed entirely when we upgrade history since v5 exposes types directly "history", // we should bump this together with react-router-dom (see https://github.com/grafana/grafana/issues/76744) + "react-router", // we should bump this together with history and react-router-dom "react-router-dom", // we should bump this together with history (see https://github.com/grafana/grafana/issues/76744) "loader-utils", // v3 requires upstream changes in ngtemplate-loader. ignore, and remove when we remove angular. "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 2dfc72663b6..e511ee0b032 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -9,6 +9,7 @@ import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Portal } from '../Portal/Portal'; import { ScrollContainer } from '../ScrollContainer/ScrollContainer'; +import { Spinner } from '../Spinner/Spinner'; import { Text } from '../Text/Text'; import { Tooltip } from '../Tooltip'; @@ -34,7 +35,7 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { options, placeholder, onChange, value, width } = props; + const { options, placeholder, onChange, value, width, invalid, loading, disabled } = props; const isAsync = typeof options === 'function'; const selectedItems = useMemo(() => { @@ -59,9 +60,13 @@ export const MultiCombobox = (props: MultiComboboxPro const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); - const { measureRef, suffixMeasureRef, shownItems } = useMeasureMulti(selectedItems, width); + const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( + selectedItems, + width, + disabled + ); const isOptionSelected = useCallback( (item: ComboboxOption) => selectedItems.some((opt) => opt.value === item.value), @@ -157,13 +162,14 @@ export const MultiCombobox = (props: MultiComboboxPro
selectedItems.length > 0 && setIsOpen(!isOpen)} + onClick={() => !disabled && selectedItems.length > 0 && setIsOpen(!isOpen)} > {visibleItems.map((item, index) => ( { removeSelectedItem(item); }} @@ -174,7 +180,7 @@ export const MultiCombobox = (props: MultiComboboxPro ))} {selectedItems.length > shownItems && !isOpen && ( - + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} ... (props: MultiComboboxPro })} {...getInputProps( getDropdownProps({ + disabled, preventKeyAction: isOpen, placeholder: selectedItems.length > 0 ? undefined : placeholder, onFocus: () => setIsOpen(true), }) )} /> + {loading && ( +
+ +
+ )}
diff --git a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx index 789436938b8..96809d281b7 100644 --- a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx +++ b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx @@ -9,28 +9,35 @@ import { IconButton } from '../IconButton/IconButton'; interface ValuePillProps { children: string; onRemove: () => void; + disabled?: boolean; } -export const ValuePill = forwardRef(({ children, onRemove, ...rest }, ref) => { - const styles = useStyles2(getValuePillStyles); - return ( - - {children} - - { - e.stopPropagation(); - onRemove(); - }} - /> - - ); -}); +export const ValuePill = forwardRef( + ({ children, onRemove, disabled, ...rest }, ref) => { + const styles = useStyles2(getValuePillStyles, disabled); + return ( + + {children} + {!disabled && ( + <> + + { + e.stopPropagation(); + onRemove(); + }} + /> + + )} + + ); + } +); -const getValuePillStyles = (theme: GrafanaTheme2) => ({ +const getValuePillStyles = (theme: GrafanaTheme2, disabled?: boolean) => ({ wrapper: css({ display: 'inline-flex', gap: theme.spacing(0.5), @@ -38,6 +45,7 @@ const getValuePillStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.text.primary, background: theme.colors.background.secondary, padding: theme.spacing(0.25), + border: disabled ? `1px solid ${theme.colors.border.weak}` : 'none', fontSize: theme.typography.bodySmall.fontSize, flexShrink: 0, minWidth: '50px', diff --git a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts index 4f7551ddf2e..107a881cf5c 100644 --- a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts @@ -5,8 +5,13 @@ import { GrafanaTheme2 } from '@grafana/data'; import { getFocusStyles } from '../../themes/mixins'; import { getInputStyles } from '../Input/Input'; -export const getMultiComboboxStyles = (theme: GrafanaTheme2, isOpen: boolean) => { - const inputStyles = getInputStyles({ theme }); +export const getMultiComboboxStyles = ( + theme: GrafanaTheme2, + isOpen: boolean, + invalid?: boolean, + disabled?: boolean +) => { + const inputStyles = getInputStyles({ theme, invalid }); const focusStyles = getFocusStyles(theme); return { @@ -52,6 +57,7 @@ export const getMultiComboboxStyles = (theme: GrafanaTheme2, isOpen: boolean) => alignItems: 'center', justifyContent: 'center', padding: theme.spacing(0, 1), + border: disabled ? `1px solid ${theme.colors.border.weak}` : 'none', borderRadius: theme.shape.radius.default, backgroundColor: theme.colors.background.secondary, cursor: 'pointer', @@ -59,5 +65,7 @@ export const getMultiComboboxStyles = (theme: GrafanaTheme2, isOpen: boolean) => backgroundColor: theme.colors.action.hover, }, }), + suffix: inputStyles.suffix, + disabled: inputStyles.inputDisabled, }; }; diff --git a/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts b/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts index 83a5b0e2871..79a7722df4d 100644 --- a/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts +++ b/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts @@ -7,26 +7,31 @@ import { ComboboxOption } from './Combobox'; const FONT_SIZE = 12; const EXTRA_PILL_SIZE = 50; +const EXTRA_PILL_DISABLED_SIZE = 10; /** * Updates the number of shown items in the multi combobox based on the available width. */ export function useMeasureMulti( selectedItems: Array>, - width?: number | 'auto' + width?: number | 'auto', + disabled?: boolean ) { const [shownItems, setShownItems] = useState(selectedItems.length); const [measureRef, { width: containerWidth }] = useMeasure(); + const [counterMeasureRef, { width: counterWidth }] = useMeasure(); const [suffixMeasureRef, { width: suffixWidth }] = useMeasure(); const finalWidth = width && width !== 'auto' ? width : containerWidth; useEffect(() => { - const maxWidth = finalWidth - suffixWidth; + const maxWidth = finalWidth - counterWidth - suffixWidth; let currWidth = 0; for (let i = 0; i < selectedItems.length; i++) { // Measure text width and add size of padding, separator and close button - currWidth += measureText(selectedItems[i].label || '', FONT_SIZE).width + EXTRA_PILL_SIZE; + currWidth += + measureText(selectedItems[i].label || '', FONT_SIZE).width + + (disabled ? EXTRA_PILL_DISABLED_SIZE : EXTRA_PILL_SIZE); if (currWidth > maxWidth) { // If there is no space for that item, show the current number of items, // but always show at least 1 item @@ -38,7 +43,7 @@ export function useMeasureMulti( setShownItems(selectedItems.length); } } - }, [finalWidth, suffixWidth, selectedItems, setShownItems]); + }, [finalWidth, counterWidth, suffixWidth, selectedItems, setShownItems, disabled]); - return { measureRef, suffixMeasureRef, shownItems }; + return { measureRef, counterMeasureRef, suffixMeasureRef, shownItems }; } diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx index 83fd5d378b1..752d9e71101 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { render, screen, waitFor, within } from 'test/test-utils'; +import { render, screen, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils'; import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; @@ -32,12 +32,15 @@ function renderAlertLabels() { ); } -function renderLabelsWithSuggestions() { - return render( +async function renderLabelsWithSuggestions() { + const view = render( ); + await waitForElementToBeRemoved(() => screen.queryByText('Loading existing labels')); + + return view; } const grafanaRule = getGrafanaRule(undefined, { @@ -71,8 +74,10 @@ describe('LabelsField with suggestions', () => { }); }); + jest.retryTimes(2); + it('Should display two dropdowns with the existing labels', async () => { - renderLabelsWithSuggestions(); + await renderLabelsWithSuggestions(); await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); @@ -86,21 +91,19 @@ describe('LabelsField with suggestions', () => { }); it('Should delete a key-value combination', async () => { - const { user } = renderLabelsWithSuggestions(); + const { user } = await renderLabelsWithSuggestions(); - await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); - - expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2); - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2); + expect(await screen.findAllByTestId('alertlabel-key-picker')).toHaveLength(2); + expect(await screen.findAllByTestId('alertlabel-value-picker')).toHaveLength(2); await user.click(screen.getByTestId('delete-label-1')); - expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1); - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(1); + expect(await screen.findAllByTestId('alertlabel-key-picker')).toHaveLength(1); + expect(await screen.findAllByTestId('alertlabel-value-picker')).toHaveLength(1); }); it('Should add new key-value dropdowns', async () => { - const { user } = renderLabelsWithSuggestions(); + const { user } = await renderLabelsWithSuggestions(); await waitFor(() => expect(screen.getByText('Add more')).toBeVisible()); await user.click(screen.getByText('Add more')); @@ -119,7 +122,7 @@ describe('LabelsField with suggestions', () => { }); it('Should be able to write new keys and values using the dropdowns', async () => { - const { user } = renderLabelsWithSuggestions(); + const { user } = await renderLabelsWithSuggestions(); await waitFor(() => expect(screen.getByText('Add more')).toBeVisible()); await user.click(screen.getByText('Add more')); @@ -134,7 +137,7 @@ describe('LabelsField with suggestions', () => { expect(screen.getByTestId('labelsInSubform-value-2')).toHaveTextContent('value3'); }); it('Should be able to write new keys and values using the dropdowns, case sensitive', async () => { - const { user } = renderLabelsWithSuggestions(); + const { user } = await renderLabelsWithSuggestions(); await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); expect(screen.getByTestId('labelsInSubform-key-0')).toHaveTextContent('key1');