Alerting: allows clearing the datasource selection in rule list (#41264)

This commit is contained in:
Gilles De Mey 2021-11-09 18:20:36 +01:00 committed by GitHub
parent f45eb309ef
commit 2bc30daa49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 109 additions and 20 deletions

View File

@ -39,6 +39,8 @@
"@grafana/tsconfig": "^1.0.0-rc1",
"@rollup/plugin-commonjs": "21.0.1",
"@rollup/plugin-node-resolve": "13.0.6",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/angular": "1.6.56",
"@types/history": "^4.7.8",
"@types/jest": "27.0.2",

View File

@ -0,0 +1,27 @@
import React from 'react';
import { DataSourcePicker } from './DataSourcePicker';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
jest.mock('../services/dataSourceSrv');
describe('DataSourcePicker', () => {
describe('onClear', () => {
it('should call onClear when function is passed', async () => {
const onClear = jest.fn();
const select = render(<DataSourcePicker onClear={onClear} />);
const clearButton = select.getByLabelText('select-clear-value');
userEvent.click(clearButton);
expect(onClear).toHaveBeenCalled();
});
it('should not render clear button when no onClear function is passed', async () => {
const select = render(<DataSourcePicker />);
expect(() => {
select.getByLabelText('select-clear-value');
}).toThrowError();
});
});
});

View File

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react';
// Components
import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui';
import { ActionMeta, HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui';
import {
DataSourceInstanceSettings,
DataSourceRef,
@ -40,6 +40,7 @@ export interface DataSourcePickerProps {
noDefault?: boolean;
width?: number;
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
onClear?: () => void;
}
/**
@ -80,7 +81,12 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
}
}
onChange = (item: SelectableValue<string>) => {
onChange = (item: SelectableValue<string>, actionMeta: ActionMeta) => {
if (actionMeta.action === 'clear' && this.props.onClear) {
this.props.onClear();
return;
}
const dsSettings = this.dataSourceSrv.getInstanceSettings(item.value);
if (dsSettings) {
@ -142,11 +148,12 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
}
render() {
const { autoFocus, onBlur, openMenuOnFocus, placeholder, width } = this.props;
const { autoFocus, onBlur, onClear, openMenuOnFocus, placeholder, width } = this.props;
const { error } = this.state;
const options = this.getDataSourceOptions();
const value = this.getCurrentValue();
const styles = getStyles();
const isClearable = typeof onClear === 'function';
return (
<div aria-label={selectors.components.DataSourcePicker.container}>
@ -156,7 +163,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
menuShouldPortal
className={styles.select}
isMulti={false}
isClearable={false}
isClearable={isClearable}
backspaceRemovesValue={false}
onChange={this.onChange}
options={options}

View File

@ -0,0 +1,22 @@
const ds1 = {
id: 1,
uid: 'c8eceabb-0275-4108-8f03-8f74faf4bf6d',
type: 'prometheus',
name: 'gdev-prometheus',
meta: {
info: {
logos: {
small: 'http://example.com/logo.png',
},
},
},
jsonData: {},
access: 'proxy',
};
export function getDataSourceSrv() {
return {
getList: () => [ds1],
getInstanceSettings: () => ds1,
};
}

View File

@ -202,10 +202,10 @@ describe('SelectBase', () => {
expect(selectEl).toBeInTheDocument();
await selectOptionInTest(selectEl, 'Option 2');
expect(spy).toHaveBeenCalledWith({
label: 'Option 2',
value: 2,
});
expect(spy).toHaveBeenCalledWith(
{ label: 'Option 2', value: 2 },
{ action: 'select-option', name: undefined, option: undefined }
);
});
});
});

View File

@ -20,7 +20,7 @@ import { MultiValueContainer, MultiValueRemove } from './MultiValue';
import { useTheme2 } from '../../themes';
import { getSelectStyles } from './getSelectStyles';
import { cleanValue, findSelectedValue } from './utils';
import { SelectBaseProps, SelectValue } from './types';
import { ActionMeta, SelectBaseProps, SelectValue } from './types';
import { deprecationWarning } from '@grafana/data';
interface ExtraValuesIndicatorProps {
@ -146,11 +146,11 @@ export function SelectBase<T>({
const theme = useTheme2();
const styles = getSelectStyles(theme);
const onChangeWithEmpty = useCallback(
(value: SelectValue<T>) => {
(value: SelectValue<T>, action: ActionMeta) => {
if (isMulti && (value === undefined || value === null)) {
return onChange([]);
return onChange([], action);
}
onChange(value);
onChange(value, action);
},
[isMulti, onChange]
);

View File

@ -1,7 +1,9 @@
import { SelectableValue } from '@grafana/data';
import React from 'react';
import { ActionMeta as SelectActionMeta } from 'react-select';
export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
export type ActionMeta = SelectActionMeta<{}>;
export type InputActionMeta = {
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
};
@ -52,7 +54,7 @@ export interface SelectCommonProps<T> {
/** The message to display when no options could be found */
noOptionsMessage?: string;
onBlur?: () => void;
onChange: (value: SelectableValue<T>) => {} | void;
onChange: (value: SelectableValue<T>, actionMeta: ActionMeta) => {} | void;
onCloseMenu?: () => void;
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */
onCreateOption?: (value: string) => void;

View File

@ -4,3 +4,4 @@ export * from './completion';
export * from './storybook';
export * from './forms';
export * from './icon';
export * from './select';

View File

@ -0,0 +1 @@
export { ActionMeta } from '../components/Select/types';

View File

@ -36,7 +36,7 @@ describe('MetricSelect', () => {
const wrapper = shallow(<MetricSelect {...props} />);
const select = wrapper.find(Select);
select.props().onChange({ value: 'foo' });
select.props().onChange({ value: 'foo' }, { action: 'select-option', option: undefined });
expect(select.props().noOptionsMessage).toBeDefined();

View File

@ -53,6 +53,10 @@ const RulesFilter = () => {
setQueryParams({ dataSource: dataSourceValue.name });
};
const clearDataSource = () => {
setQueryParams({ dataSource: null });
};
const handleQueryStringChange = debounce((e: FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
setQueryParams({ queryString: target.value || null });
@ -84,13 +88,15 @@ const RulesFilter = () => {
return (
<div className={styles.container}>
<div className={styles.inputWidth}>
<Label>Select data source</Label>
<Label>Search by data source</Label>
<DataSourcePicker
key={dataSourceKey}
alerting
noDefault
placeholder="All data sources"
current={dataSource}
onChange={handleDataSourceChange}
onClear={clearDataSource}
/>
</div>
<div className={cx(styles.flexRow, styles.spaceBetween)}>

View File

@ -74,7 +74,10 @@ describe('useCreatableSelectPersistedBehaviour', () => {
// Should call onChange when selecting an already existing option
userEvent.click(option1);
expect(onChange).toHaveBeenLastCalledWith({ value: 'Option 1', label: 'Option 1' });
expect(onChange).toHaveBeenLastCalledWith(
{ value: 'Option 1', label: 'Option 1' },
{ action: 'select-option', name: undefined, option: undefined }
);
userEvent.click(input);

View File

@ -27,7 +27,7 @@ describe('ElasticDetails', () => {
const onChangeMock = jest.fn();
const wrapper = mount(<ElasticDetails onChange={onChangeMock} value={createDefaultConfigOptions()} />);
const selectEl = wrapper.find({ label: 'Pattern' }).find(Select);
selectEl.props().onChange({ value: 'Daily', label: 'Daily' });
selectEl.props().onChange({ value: 'Daily', label: 'Daily' }, { action: 'select-option', option: undefined });
expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Daily');
expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM.DD');
@ -40,7 +40,7 @@ describe('ElasticDetails', () => {
const wrapper = mount(<ElasticDetails onChange={onChangeMock} value={options} />);
const selectEl = wrapper.find({ label: 'Pattern' }).find(Select);
selectEl.props().onChange({ value: 'Monthly', label: 'Monthly' });
selectEl.props().onChange({ value: 'Monthly', label: 'Monthly' }, { action: 'select-option', option: undefined });
expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Monthly');
expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM');
@ -78,7 +78,12 @@ describe('ElasticDetails', () => {
});
const selectEl = wrapper.find({ label: 'Version' }).find(Select);
selectEl.props().onChange({ value: tc.version, label: tc.version.toString() });
selectEl
.props()
.onChange(
{ value: tc.version, label: tc.version.toString() },
{ action: 'select-option', option: undefined }
);
expect(last(onChangeMock.mock.calls)[0].jsonData.maxConcurrentShardRequests).toBe(
tc.expectedMaxConcurrentShardRequests

View File

@ -2508,6 +2508,8 @@ __metadata:
"@rollup/plugin-commonjs": 21.0.1
"@rollup/plugin-node-resolve": 13.0.6
"@sentry/browser": 5.25.0
"@testing-library/react": ^12.1.2
"@testing-library/user-event": ^13.5.0
"@types/angular": 1.6.56
"@types/history": ^4.7.8
"@types/jest": 27.0.2
@ -6845,7 +6847,7 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/react@npm:12.1.2":
"@testing-library/react@npm:12.1.2, @testing-library/react@npm:^12.1.2":
version: 12.1.2
resolution: "@testing-library/react@npm:12.1.2"
dependencies:
@ -6869,6 +6871,17 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/user-event@npm:^13.5.0":
version: 13.5.0
resolution: "@testing-library/user-event@npm:13.5.0"
dependencies:
"@babel/runtime": ^7.12.5
peerDependencies:
"@testing-library/dom": ">=7.21.4"
checksum: 16319de685fbb7008f1ba667928f458b2d08196918002daca56996de80ef35e6d9de26e9e1ece7d00a004692b95a597cf9142fff0dc53f2f51606a776584f549
languageName: node
linkType: hard
"@tootallnate/once@npm:1":
version: 1.1.2
resolution: "@tootallnate/once@npm:1.1.2"