mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: allows clearing the datasource selection in rule list (#41264)
This commit is contained in:
parent
f45eb309ef
commit
2bc30daa49
@ -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",
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -4,3 +4,4 @@ export * from './completion';
|
||||
export * from './storybook';
|
||||
export * from './forms';
|
||||
export * from './icon';
|
||||
export * from './select';
|
||||
|
1
packages/grafana-ui/src/types/select.ts
Normal file
1
packages/grafana-ui/src/types/select.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ActionMeta } from '../components/Select/types';
|
@ -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();
|
||||
|
||||
|
@ -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)}>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
15
yarn.lock
15
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user