diff --git a/.betterer.results b/.betterer.results
index c78641294f7..d9317283aca 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -682,11 +682,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "5"],
- [0, 0, 0, "Do not use any type assertions.", "6"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "7"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "8"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "9"]
+ [0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx
index 9a9387df0f3..5e8fed16607 100644
--- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx
+++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx
@@ -174,35 +174,20 @@ describe('PromQueryBuilder', () => {
});
it('shows hints for histogram metrics', async () => {
- const { container } = setup({
+ setup({
metric: 'histogram_metric_bucket',
labels: [],
operations: [],
});
- await openMetricSelect(container);
- await userEvent.click(screen.getByText('histogram_metric_bucket'));
await waitFor(() => expect(screen.getByText('hint: add histogram_quantile')).toBeInTheDocument());
});
it('shows hints for counter metrics', async () => {
- const { container } = setup({
+ setup({
metric: 'histogram_metric_sum',
labels: [],
operations: [],
});
- await openMetricSelect(container);
- await userEvent.click(screen.getByText('histogram_metric_sum'));
- await waitFor(() => expect(screen.getByText('hint: add rate')).toBeInTheDocument());
- });
-
- it('shows hints for counter metrics', async () => {
- const { container } = setup({
- metric: 'histogram_metric_sum',
- labels: [],
- operations: [],
- });
- await openMetricSelect(container);
- await userEvent.click(screen.getByText('histogram_metric_sum'));
await waitFor(() => expect(screen.getByText('hint: add rate')).toBeInTheDocument());
});
@@ -215,7 +200,7 @@ describe('PromQueryBuilder', () => {
for (let i = 0; i < 25; i++) {
data.series.push(new MutableDataFrame());
}
- const { container } = setup(
+ setup(
{
metric: 'histogram_metric_sum',
labels: [],
@@ -223,8 +208,6 @@ describe('PromQueryBuilder', () => {
},
data
);
- await openMetricSelect(container);
- await userEvent.click(screen.getByText('histogram_metric_sum'));
await waitFor(() => expect(screen.getAllByText(/hint:/)).toHaveLength(2));
});
diff --git a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx
index a17c1e6e326..ac152ab7892 100644
--- a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx
+++ b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx
@@ -64,6 +64,39 @@ describe('SelectBase', () => {
expect(screen.queryByText('Test label')).not.toBeInTheDocument();
});
+ describe('with custom values', () => {
+ it('allows editing a custom SelectableValue', async () => {
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole('combobox'));
+ await userEvent.type(screen.getByRole('combobox'), '{backspace}{backspace}{enter}');
+ expect(onChangeHandler).toHaveBeenCalled();
+ expect(onChangeHandler.mock.calls[0][0]).toEqual(
+ expect.objectContaining({ label: 'my custom val', value: 'my custom val' })
+ );
+ });
+
+ it('allows editing a custom basic value', async () => {
+ render();
+
+ await userEvent.click(screen.getByRole('combobox'));
+ await userEvent.type(screen.getByRole('combobox'), '{backspace}{backspace}{enter}');
+ expect(onChangeHandler).toHaveBeenCalled();
+ expect(onChangeHandler.mock.calls[0][0]).toEqual(
+ expect.objectContaining({ label: 'my custom val', value: 'my custom val' })
+ );
+ });
+ });
+
describe('when openMenuOnFocus prop', () => {
describe('is provided', () => {
it('opens on focus', () => {
diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx
index 995098d7448..eef3936cfaa 100644
--- a/packages/grafana-ui/src/components/Select/SelectBase.tsx
+++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx
@@ -1,6 +1,11 @@
import { t } from 'i18next';
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
-import { default as ReactSelect, IndicatorsContainerProps, Props as ReactSelectProps } from 'react-select';
+import {
+ default as ReactSelect,
+ IndicatorsContainerProps,
+ Props as ReactSelectProps,
+ ClearIndicatorProps,
+} from 'react-select';
import { default as ReactAsyncSelect } from 'react-select/async';
import { default as AsyncCreatable } from 'react-select/async-creatable';
import Creatable from 'react-select/creatable';
@@ -19,7 +24,7 @@ import { MultiValueContainer, MultiValueRemove } from './MultiValue';
import { SelectContainer } from './SelectContainer';
import { SelectMenu, SelectMenuOptions, VirtualizedSelectMenu } from './SelectMenu';
import { SelectOptionGroup } from './SelectOptionGroup';
-import { SingleValue } from './SingleValue';
+import { Props, SingleValue } from './SingleValue';
import { ValueContainer } from './ValueContainer';
import { getSelectStyles } from './getSelectStyles';
import { useCustomSelectStyles } from './resetSelectStyles';
@@ -199,6 +204,8 @@ export function SelectBase({
}
}
+ const [internalInputValue, setInternalInputValue] = useState('');
+
const commonSelectProps = {
'aria-label': ariaLabel,
'data-testid': dataTestid,
@@ -268,12 +275,41 @@ export function SelectBase({
};
if (allowCustomValue) {
- ReactSelectComponent = Creatable as any;
+ ReactSelectComponent = Creatable;
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
creatableProps.formatCreateLabel = formatCreateLabel ?? defaultFormatCreateLabel;
creatableProps.onCreateOption = onCreateOption;
creatableProps.createOptionPosition = createOptionPosition;
creatableProps.isValidNewOption = isValidNewOption;
+
+ // code needed to allow editing a custom value once entered
+ // we only want to do this for single selects, not multi
+ if (!isMulti) {
+ creatableProps.inputValue = internalInputValue;
+ creatableProps.onMenuOpen = () => {
+ // make sure we call the base onMenuOpen if it exists
+ commonSelectProps.onMenuOpen?.();
+
+ // restore the input state to the selected value
+ setInternalInputValue(selectedValue?.[0]?.label ?? '');
+ };
+ creatableProps.onInputChange = (val, actionMeta) => {
+ // make sure we call the base onInputChange
+ commonSelectProps.onInputChange(val, actionMeta);
+
+ // update the input value state on change since we're explicitly controlling it
+ if (actionMeta.action === 'input-change') {
+ setInternalInputValue(val);
+ }
+ };
+ creatableProps.onMenuClose = () => {
+ // make sure we call the base onMenuClose if it exists
+ commonSelectProps.onMenuClose?.();
+
+ // clear the input state on menu close, else react-select won't show the SingleValue component correctly
+ setInternalInputValue('');
+ };
+ }
}
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
@@ -300,7 +336,7 @@ export function SelectBase({
IndicatorSeparator: IndicatorSeparator,
Control: CustomControl,
Option: SelectMenuOptions,
- ClearIndicator(props: any) {
+ ClearIndicator(props: ClearIndicatorProps) {
const { clearValue } = props;
return (
({
);
},
DropdownIndicator: DropdownIndicator,
- SingleValue(props: any) {
+ SingleValue(props: Props) {
return ;
},
SelectContainer,
diff --git a/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx
index 20049f5a7ed..bc5d5eb529d 100644
--- a/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx
+++ b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx
@@ -111,6 +111,7 @@ describe('useCreatableSelectPersistedBehaviour', () => {
await userEvent.click(input);
// we expect 2 elemnts having "Option 2": the input itself and the option.
- expect(screen.getAllByText('Option 2')).toHaveLength(2);
+ expect(screen.getByText('Option 2')).toBeInTheDocument();
+ expect(screen.getByRole('combobox')).toHaveValue('Option 2');
});
});
diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
index bc1bae92086..9ff8bfa1ab9 100644
--- a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
+++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
@@ -157,6 +157,7 @@ describe('SearchForm', () => {
await user.click(asyncOperationSelect);
jest.advanceTimersByTime(3000);
+ await user.clear(asyncOperationSelect);
await user.type(asyncOperationSelect, '$');
const operationOption = await screen.findByText('$operation');
expect(operationOption).toBeDefined();