Datasource/CloudWatch: Allows a user to search for log groups that aren't there initially (#24695)
Closes #24554
This commit is contained in:
@@ -2,6 +2,9 @@ import { SelectableValue } from '@grafana/data';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
|
export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
|
||||||
|
export type InputActionMeta = {
|
||||||
|
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
|
||||||
|
};
|
||||||
|
|
||||||
export interface SelectCommonProps<T> {
|
export interface SelectCommonProps<T> {
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
@@ -39,7 +42,7 @@ export interface SelectCommonProps<T> {
|
|||||||
onCloseMenu?: () => void;
|
onCloseMenu?: () => void;
|
||||||
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */
|
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */
|
||||||
onCreateOption?: (value: string) => void;
|
onCreateOption?: (value: string) => void;
|
||||||
onInputChange?: (label: string) => void;
|
onInputChange?: (value: string, actionMeta: InputActionMeta) => void;
|
||||||
onKeyDown?: (event: React.KeyboardEvent) => void;
|
onKeyDown?: (event: React.KeyboardEvent) => void;
|
||||||
onOpenMenu?: () => void;
|
onOpenMenu?: () => void;
|
||||||
openMenuOnFocus?: boolean;
|
openMenuOnFocus?: boolean;
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { CloudWatchLogsQueryField } from './LogsQueryField';
|
import { CloudWatchLogsQueryField } from './LogsQueryField';
|
||||||
import { ExploreId } from '../../../../types';
|
import { ExploreId } from '../../../../types';
|
||||||
|
import { DescribeLogGroupsRequest } from '../types';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
jest.mock('lodash/debounce', () => {
|
||||||
|
const fakeDebounce = (func: () => any, period: number) => func;
|
||||||
|
return fakeDebounce;
|
||||||
|
});
|
||||||
|
|
||||||
describe('CloudWatchLogsQueryField', () => {
|
describe('CloudWatchLogsQueryField', () => {
|
||||||
it('updates upstream query log groups on region change', async () => {
|
it('updates upstream query log groups on region change', async () => {
|
||||||
@@ -58,4 +65,151 @@ describe('CloudWatchLogsQueryField', () => {
|
|||||||
// Make sure we correctly updated the upstream state
|
// Make sure we correctly updated the upstream state
|
||||||
expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual({ region: 'region2', logGroupNames: [] });
|
expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual({ region: 'region2', logGroupNames: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should merge results of remote log groups search with existing results', async () => {
|
||||||
|
const allLogGroups = [
|
||||||
|
'AmazingGroup',
|
||||||
|
'AmazingGroup2',
|
||||||
|
'AmazingGroup3',
|
||||||
|
'BeautifulGroup',
|
||||||
|
'BeautifulGroup2',
|
||||||
|
'BeautifulGroup3',
|
||||||
|
'CrazyGroup',
|
||||||
|
'CrazyGroup2',
|
||||||
|
'CrazyGroup3',
|
||||||
|
'DeliciousGroup',
|
||||||
|
'DeliciousGroup2',
|
||||||
|
'DeliciousGroup3',
|
||||||
|
'EnjoyableGroup',
|
||||||
|
'EnjoyableGroup2',
|
||||||
|
'EnjoyableGroup3',
|
||||||
|
'FavouriteGroup',
|
||||||
|
'FavouriteGroup2',
|
||||||
|
'FavouriteGroup3',
|
||||||
|
'GorgeousGroup',
|
||||||
|
'GorgeousGroup2',
|
||||||
|
'GorgeousGroup3',
|
||||||
|
'HappyGroup',
|
||||||
|
'HappyGroup2',
|
||||||
|
'HappyGroup3',
|
||||||
|
'IncredibleGroup',
|
||||||
|
'IncredibleGroup2',
|
||||||
|
'IncredibleGroup3',
|
||||||
|
'JollyGroup',
|
||||||
|
'JollyGroup2',
|
||||||
|
'JollyGroup3',
|
||||||
|
'KoolGroup',
|
||||||
|
'KoolGroup2',
|
||||||
|
'KoolGroup3',
|
||||||
|
'LovelyGroup',
|
||||||
|
'LovelyGroup2',
|
||||||
|
'LovelyGroup3',
|
||||||
|
'MagnificentGroup',
|
||||||
|
'MagnificentGroup2',
|
||||||
|
'MagnificentGroup3',
|
||||||
|
'NiceGroup',
|
||||||
|
'NiceGroup2',
|
||||||
|
'NiceGroup3',
|
||||||
|
'OddGroup',
|
||||||
|
'OddGroup2',
|
||||||
|
'OddGroup3',
|
||||||
|
'PerfectGroup',
|
||||||
|
'PerfectGroup2',
|
||||||
|
'PerfectGroup3',
|
||||||
|
'QuietGroup',
|
||||||
|
'QuietGroup2',
|
||||||
|
'QuietGroup3',
|
||||||
|
'RestlessGroup',
|
||||||
|
'RestlessGroup2',
|
||||||
|
'RestlessGroup3',
|
||||||
|
'SurpriseGroup',
|
||||||
|
'SurpriseGroup2',
|
||||||
|
'SurpriseGroup3',
|
||||||
|
'TestingGroup',
|
||||||
|
'TestingGroup2',
|
||||||
|
'TestingGroup3',
|
||||||
|
'UmbrellaGroup',
|
||||||
|
'UmbrellaGroup2',
|
||||||
|
'UmbrellaGroup3',
|
||||||
|
'VelvetGroup',
|
||||||
|
'VelvetGroup2',
|
||||||
|
'VelvetGroup3',
|
||||||
|
'WaterGroup',
|
||||||
|
'WaterGroup2',
|
||||||
|
'WaterGroup3',
|
||||||
|
'XylophoneGroup',
|
||||||
|
'XylophoneGroup2',
|
||||||
|
'XylophoneGroup3',
|
||||||
|
'YellowGroup',
|
||||||
|
'YellowGroup2',
|
||||||
|
'YellowGroup3',
|
||||||
|
'ZebraGroup',
|
||||||
|
'ZebraGroup2',
|
||||||
|
'ZebraGroup3',
|
||||||
|
];
|
||||||
|
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const wrapper = shallow<CloudWatchLogsQueryField>(
|
||||||
|
<CloudWatchLogsQueryField
|
||||||
|
history={[]}
|
||||||
|
absoluteRange={{ from: 1, to: 10 }}
|
||||||
|
syntaxLoaded={false}
|
||||||
|
syntax={{} as any}
|
||||||
|
exploreId={ExploreId.left}
|
||||||
|
datasource={
|
||||||
|
{
|
||||||
|
getRegions() {
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: 'region1',
|
||||||
|
value: 'region1',
|
||||||
|
text: 'region1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'region2',
|
||||||
|
value: 'region2',
|
||||||
|
text: 'region2',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
describeLogGroups(params: DescribeLogGroupsRequest) {
|
||||||
|
const theLogGroups = allLogGroups
|
||||||
|
.filter(logGroupName => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
|
||||||
|
.slice(0, Math.max(params.limit ?? 50, 50));
|
||||||
|
return Promise.resolve(theLogGroups);
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
query={{} as any}
|
||||||
|
onRunQuery={() => {}}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialAvailableGroups = allLogGroups
|
||||||
|
.slice(0, 50)
|
||||||
|
.map(logGroupName => ({ value: logGroupName, label: logGroupName }));
|
||||||
|
wrapper.setState({
|
||||||
|
availableLogGroups: initialAvailableGroups,
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.instance().onLogGroupSearch('Water', 'default', { action: 'input-change' });
|
||||||
|
|
||||||
|
let nextAvailableGroups = (wrapper.state('availableLogGroups') as Array<SelectableValue<string>>).map(
|
||||||
|
logGroup => logGroup.value
|
||||||
|
);
|
||||||
|
expect(nextAvailableGroups).toEqual(
|
||||||
|
initialAvailableGroups.map(logGroup => logGroup.value).concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3'])
|
||||||
|
);
|
||||||
|
|
||||||
|
await wrapper.instance().onLogGroupSearch('Velv', 'default', { action: 'input-change' });
|
||||||
|
nextAvailableGroups = (wrapper.state('availableLogGroups') as Array<SelectableValue<string>>).map(
|
||||||
|
logGroup => logGroup.value
|
||||||
|
);
|
||||||
|
expect(nextAvailableGroups).toEqual(
|
||||||
|
initialAvailableGroups
|
||||||
|
.map(logGroup => logGroup.value)
|
||||||
|
.concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3'])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import intersectionBy from 'lodash/intersectionBy';
|
import intersectionBy from 'lodash/intersectionBy';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import unionBy from 'lodash/unionBy';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QueryField,
|
QueryField,
|
||||||
@@ -31,6 +32,7 @@ import { ExploreId } from 'app/types';
|
|||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { changeModeAction } from 'app/features/explore/state/actionTypes';
|
import { changeModeAction } from 'app/features/explore/state/actionTypes';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
|
import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
|
||||||
|
|
||||||
export interface CloudWatchLogsQueryFieldProps extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery> {
|
export interface CloudWatchLogsQueryFieldProps extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery> {
|
||||||
absoluteRange: AbsoluteTimeRange;
|
absoluteRange: AbsoluteTimeRange;
|
||||||
@@ -104,11 +106,12 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchLogGroupOptions = async (region: string) => {
|
fetchLogGroupOptions = async (region: string, logGroupNamePrefix?: string) => {
|
||||||
try {
|
try {
|
||||||
const logGroups: string[] = await this.props.datasource.describeLogGroups({
|
const logGroups: string[] = await this.props.datasource.describeLogGroups({
|
||||||
refId: this.props.query.refId,
|
refId: this.props.query.refId,
|
||||||
region,
|
region,
|
||||||
|
logGroupNamePrefix,
|
||||||
});
|
});
|
||||||
|
|
||||||
return logGroups.map(logGroup => ({
|
return logGroups.map(logGroup => ({
|
||||||
@@ -121,6 +124,30 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLogGroupSearch = (searchTerm: string, region: string, actionMeta: InputActionMeta) => {
|
||||||
|
if (actionMeta.action !== 'input-change') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loadingLogGroups: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.fetchLogGroupOptions(region, searchTerm)
|
||||||
|
.then(matchingLogGroups => {
|
||||||
|
this.setState(state => ({
|
||||||
|
availableLogGroups: unionBy(state.availableLogGroups, matchingLogGroups, 'value'),
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({
|
||||||
|
loadingLogGroups: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onLogGroupSearchDebounced = debounce(this.onLogGroupSearch, 300);
|
||||||
|
|
||||||
componentWillMount = () => {
|
componentWillMount = () => {
|
||||||
const { datasource, query, onChange } = this.props;
|
const { datasource, query, onChange } = this.props;
|
||||||
|
|
||||||
@@ -355,6 +382,9 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
noOptionsMessage="No log groups available"
|
noOptionsMessage="No log groups available"
|
||||||
isLoading={loadingLogGroups}
|
isLoading={loadingLogGroups}
|
||||||
onOpenMenu={this.onOpenLogGroupMenu}
|
onOpenMenu={this.onOpenLogGroupMenu}
|
||||||
|
onInputChange={(value, actionMeta) => {
|
||||||
|
this.onLogGroupSearchDebounced(value, selectedRegion.value ?? 'default', actionMeta);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user