From 0e8638ec925b0d73e15a0ffc783b5a2becd5264e Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 14 May 2020 19:41:38 +0100 Subject: [PATCH] Datasource/CloudWatch: Allows a user to search for log groups that aren't there initially (#24695) Closes #24554 --- .../grafana-ui/src/components/Select/types.ts | 5 +- .../components/LogsQueryField.test.tsx | 154 ++++++++++++++++++ .../cloudwatch/components/LogsQueryField.tsx | 32 +++- 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 27caf8de145..38bfb4b6cdd 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -2,6 +2,9 @@ import { SelectableValue } from '@grafana/data'; import React from 'react'; export type SelectValue = T | SelectableValue | T[] | Array>; +export type InputActionMeta = { + action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close'; +}; export interface SelectCommonProps { allowCustomValue?: boolean; @@ -39,7 +42,7 @@ export interface SelectCommonProps { 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; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx index 2dd6e4f018a..19a180946b0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx @@ -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( + 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>).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>).map( + logGroup => logGroup.value + ); + expect(nextAvailableGroups).toEqual( + initialAvailableGroups + .map(logGroup => logGroup.value) + .concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3']) + ); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index c42c146cbe5..a15d1f44f03 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -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 { absoluteRange: AbsoluteTimeRange; @@ -104,11 +106,12 @@ export class CloudWatchLogsQueryField extends React.PureComponent { + 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 { + 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 { + this.onLogGroupSearchDebounced(value, selectedRegion.value ?? 'default', actionMeta); + }} /> } />