mirror of https://github.com/grafana/grafana.git
				
				
				
			Datasource/CloudWatch: Allows a user to search for log groups that aren't there initially (#24695)
Closes #24554
This commit is contained in:
		
							parent
							
								
									bc8c05137b
								
							
						
					
					
						commit
						0e8638ec92
					
				|  | @ -2,6 +2,9 @@ import { SelectableValue } from '@grafana/data'; | |||
| import React from 'react'; | ||||
| 
 | ||||
| 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> { | ||||
|   allowCustomValue?: boolean; | ||||
|  | @ -39,7 +42,7 @@ export interface SelectCommonProps<T> { | |||
|   onCloseMenu?: () => void; | ||||
|   /** allowCustomValue must be enabled. Function decides what to do with that custom value. */ | ||||
|   onCreateOption?: (value: string) => void; | ||||
|   onInputChange?: (label: string) => void; | ||||
|   onInputChange?: (value: string, actionMeta: InputActionMeta) => void; | ||||
|   onKeyDown?: (event: React.KeyboardEvent) => void; | ||||
|   onOpenMenu?: () => void; | ||||
|   openMenuOnFocus?: boolean; | ||||
|  |  | |||
|  | @ -2,6 +2,13 @@ import React from 'react'; | |||
| import { shallow } from 'enzyme'; | ||||
| import { CloudWatchLogsQueryField } from './LogsQueryField'; | ||||
| 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', () => { | ||||
|   it('updates upstream query log groups on region change', async () => { | ||||
|  | @ -58,4 +65,151 @@ describe('CloudWatchLogsQueryField', () => { | |||
|     // Make sure we correctly updated the upstream state
 | ||||
|     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 intersectionBy from 'lodash/intersectionBy'; | ||||
| import debounce from 'lodash/debounce'; | ||||
| import unionBy from 'lodash/unionBy'; | ||||
| 
 | ||||
| import { | ||||
|   QueryField, | ||||
|  | @ -31,6 +32,7 @@ import { ExploreId } from 'app/types'; | |||
| import { dispatch } from 'app/store/store'; | ||||
| import { changeModeAction } from 'app/features/explore/state/actionTypes'; | ||||
| import { appEvents } from 'app/core/core'; | ||||
| import { InputActionMeta } from '@grafana/ui/src/components/Select/types'; | ||||
| 
 | ||||
| export interface CloudWatchLogsQueryFieldProps extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery> { | ||||
|   absoluteRange: AbsoluteTimeRange; | ||||
|  | @ -104,11 +106,12 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs | |||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   fetchLogGroupOptions = async (region: string) => { | ||||
|   fetchLogGroupOptions = async (region: string, logGroupNamePrefix?: string) => { | ||||
|     try { | ||||
|       const logGroups: string[] = await this.props.datasource.describeLogGroups({ | ||||
|         refId: this.props.query.refId, | ||||
|         region, | ||||
|         logGroupNamePrefix, | ||||
|       }); | ||||
| 
 | ||||
|       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 = () => { | ||||
|     const { datasource, query, onChange } = this.props; | ||||
| 
 | ||||
|  | @ -355,6 +382,9 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs | |||
|                 noOptionsMessage="No log groups available" | ||||
|                 isLoading={loadingLogGroups} | ||||
|                 onOpenMenu={this.onOpenLogGroupMenu} | ||||
|                 onInputChange={(value, actionMeta) => { | ||||
|                   this.onLogGroupSearchDebounced(value, selectedRegion.value ?? 'default', actionMeta); | ||||
|                 }} | ||||
|               /> | ||||
|             } | ||||
|           /> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue