From c76620102517abc543b136a897432373ce6c4aab Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 7 Jan 2025 13:53:54 +0300 Subject: [PATCH 1/4] K8s/OpenAPI: remove unnecessary x-kubernetes-group-version-kind (#98578) --- pkg/services/apiserver/builder/openapi.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 0830af9838c..78089f44628 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -47,6 +47,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*s if s.Paths == nil { return s, nil } + for _, b := range builders { gv := b.GetGroupVersion() prefix := "/apis/" + gv.String() + "/" @@ -65,6 +66,13 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*s Paths: s.Paths, } + // Remove the growing list of kinds + for k, v := range copy.Components.Schemas { + if strings.HasPrefix(k, "io.k8s.apimachinery.pkg.apis.meta.v1") && v.Extensions != nil { + delete(v.Extensions, "x-kubernetes-group-version-kind") // a growing list of everything + } + } + // Optionally include raw http handlers provider, ok := b.(APIGroupRouteProvider) if ok && provider != nil { @@ -110,6 +118,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*s return ©, nil } } + return s, nil } } From c6b73d8aad8da9337b6817b2fd5a5c3c05a835bd Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 7 Jan 2025 11:18:53 +0000 Subject: [PATCH 2/4] Chore: ignore automatic updates to `react-router` (#98512) ignore automatic updates to react-router (this is a large manual task) --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) 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 From 96e87482660dd8acf499ebac0d01bb417f2dc221 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 7 Jan 2025 11:36:53 +0000 Subject: [PATCH 3/4] Alerting: Attempt to improve flakiness of labels field tests (#98587) --- .../rule-editor/labels/LabelsField.test.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) 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'); From 878f5957fbad97a59ea1fefb5ae5023364a04904 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:47:54 +0000 Subject: [PATCH 4/4] MultiCombobox: Add loading, invalid and disabled states (#98423) --- .../src/components/Combobox/MultiCombobox.tsx | 24 +++++++--- .../src/components/Combobox/ValuePill.tsx | 46 +++++++++++-------- .../Combobox/getMultiComboboxStyles.ts | 12 ++++- .../components/Combobox/useMeasureMulti.ts | 15 ++++-- 4 files changed, 65 insertions(+), 32 deletions(-) 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 }; }