diff --git a/.betterer.results b/.betterer.results index 3f2a4722773..bfee741b724 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5183,18 +5183,9 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index caf6e1274d8..77163253dc7 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -403,142 +403,6 @@ func TestQuery_ResourceRequest_DescribeAllLogGroups(t *testing.T) { }) } -func TestQuery_ResourceRequest_DescribeLogGroups(t *testing.T) { - origNewCWLogsClient := NewCWLogsClient - t.Cleanup(func() { - NewCWLogsClient = origNewCWLogsClient - }) - - var cli fakeCWLogsClient - - NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { - return &cli - } - - im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil - }) - - executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) - sender := &mockedCallResourceResponseSenderForOauth{} - - t.Run("Should map log groups to SuggestData response", func(t *testing.T) { - cli = fakeCWLogsClient{ - logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{ - {LogGroups: []*cloudwatchlogs.LogGroup{ - { - LogGroupName: aws.String("group_a"), - }, - { - LogGroupName: aws.String("group_b"), - }, - { - LogGroupName: aws.String("group_c"), - }, - }}, - }, - } - - req := &backend.CallResourceRequest{ - Method: "GET", - Path: "/log-groups", - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - ID: 0, - }, - PluginID: "cloudwatch", - }, - } - err := executor.CallResource(context.Background(), req, sender) - - require.NoError(t, err) - sent := sender.Response - require.NotNil(t, sent) - require.Equal(t, http.StatusOK, sent.Status) - - suggestDataResponse := []suggestData{} - err = json.Unmarshal(sent.Body, &suggestDataResponse) - require.Nil(t, err) - - assert.Equal(t, stringsToSuggestData([]string{"group_a", "group_b", "group_c"}), suggestDataResponse) - }) - - t.Run("Should call api with LogGroupNamePrefix if passed in resource call", func(t *testing.T) { - cli = fakeCWLogsClient{ - logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{ - {LogGroups: []*cloudwatchlogs.LogGroup{}}, - }, - } - - im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil - }) - - executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) - - req := &backend.CallResourceRequest{ - Method: "GET", - Path: "/log-groups?logGroupNamePrefix=test", - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - ID: 0, - }, - PluginID: "cloudwatch", - }, - } - err := executor.CallResource(context.Background(), req, sender) - - require.NoError(t, err) - sent := sender.Response - require.NotNil(t, sent) - require.Equal(t, http.StatusOK, sent.Status) - - assert.Equal(t, []*cloudwatchlogs.DescribeLogGroupsInput{ - { - Limit: aws.Int64(defaultLogGroupLimit), - LogGroupNamePrefix: aws.String("test"), - }, - }, cli.calls.describeLogGroups) - }) - - t.Run("Should call api without LogGroupNamePrefix if not passed in resource call", func(t *testing.T) { - cli = fakeCWLogsClient{ - logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{ - {LogGroups: []*cloudwatchlogs.LogGroup{}}, - }, - } - - im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil - }) - - executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) - - req := &backend.CallResourceRequest{ - Method: "GET", - Path: "/log-groups?limit=100", - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - ID: 0, - }, - PluginID: "cloudwatch", - }, - } - err := executor.CallResource(context.Background(), req, sender) - - require.NoError(t, err) - sent := sender.Response - require.NotNil(t, sent) - require.Equal(t, http.StatusOK, sent.Status) - - assert.Equal(t, []*cloudwatchlogs.DescribeLogGroupsInput{ - { - Limit: aws.Int64(100), - }, - }, cli.calls.describeLogGroups) - }) -} - func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *testing.T) { sender := &mockedCallResourceResponseSenderForOauth{} origNewMetricsAPI := NewMetricsAPI diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index a8e8d6eebee..6649c237c98 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -18,7 +18,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) @@ -213,19 +212,16 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c QueryString: aws.String(modifiedQueryString), } - if e.features.IsEnabled(featuremgmt.FlagCloudWatchCrossAccountQuerying) { - if logsQuery.LogGroups != nil && len(logsQuery.LogGroups) > 0 { - var logGroupIdentifiers []string - for _, lg := range logsQuery.LogGroups { - arn := lg.ARN - // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error - logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) - } - startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers) + if logsQuery.LogGroups != nil && len(logsQuery.LogGroups) > 0 { + var logGroupIdentifiers []string + for _, lg := range logsQuery.LogGroups { + arn := lg.ARN + // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error + logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) } - } - - if startQueryInput.LogGroupIdentifiers == nil { + startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers) + } else { + // even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames) } diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index cc4e1f3904f..237f0d4dcae 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -7,7 +7,6 @@ import ( "net/url" "reflect" "sort" - "strconv" "strings" "sync" "time" @@ -290,39 +289,6 @@ func (e *cloudWatchExecutor) resourceGroupsGetResources(pluginCtx backend.Plugin return &resp, nil } -func (e *cloudWatchExecutor) handleGetLogGroups(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { - region := parameters.Get("region") - limit := parameters.Get("limit") - logGroupNamePrefix := parameters.Get("logGroupNamePrefix") - - logsClient, err := e.getCWLogsClient(pluginCtx, region) - if err != nil { - return nil, err - } - - logGroupLimit := defaultLogGroupLimit - intLimit, err := strconv.ParseInt(limit, 10, 64) - if err == nil && intLimit > 0 { - logGroupLimit = intLimit - } - - var response *cloudwatchlogs.DescribeLogGroupsOutput = nil - input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(logGroupLimit)} - if len(logGroupNamePrefix) > 0 { - input.LogGroupNamePrefix = aws.String(logGroupNamePrefix) - } - response, err = logsClient.DescribeLogGroups(input) - if err != nil || response == nil { - return nil, err - } - result := make([]suggestData, 0) - for _, logGroup := range response.LogGroups { - logGroupName := *logGroup.LogGroupName - result = append(result, suggestData{Text: logGroupName, Value: logGroupName, Label: logGroupName}) - } - - return result, nil -} func (e *cloudWatchExecutor) handleGetAllLogGroups(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { var nextToken *string diff --git a/pkg/tsdb/cloudwatch/resource_handler.go b/pkg/tsdb/cloudwatch/resource_handler.go index 55d95b7caff..11cd293a891 100644 --- a/pkg/tsdb/cloudwatch/resource_handler.go +++ b/pkg/tsdb/cloudwatch/resource_handler.go @@ -18,8 +18,7 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux { mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds)) mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute)) mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns)) - mux.HandleFunc("/log-groups", handleResourceReq(e.handleGetLogGroups)) - mux.HandleFunc("/describe-log-groups", routes.ResourceRequestMiddleware(routes.LogGroupsHandler, logger, e.getRequestContext)) // supports CrossAccountQuerying + mux.HandleFunc("/describe-log-groups", routes.ResourceRequestMiddleware(routes.LogGroupsHandler, logger, e.getRequestContext)) mux.HandleFunc("/all-log-groups", handleResourceReq(e.handleGetAllLogGroups)) mux.HandleFunc("/metrics", routes.ResourceRequestMiddleware(routes.MetricsHandler, logger, e.getRequestContext)) mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, logger, e.getRequestContext)) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index a108422b18c..5907a154e75 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -48,7 +48,7 @@ export const meta: DataSourcePluginMeta = { }; export const CloudWatchSettings: DataSourceInstanceSettings = { - jsonData: { defaultRegion: 'us-west-1', tracingDatasourceUid: 'xray' }, + jsonData: { defaultRegion: 'us-west-1', tracingDatasourceUid: 'xray', logGroups: [] }, id: 0, uid: '', type: '', @@ -80,13 +80,12 @@ export function setupMockedDataSource({ const timeSrv = getTimeSrv(); const datasource = new CloudWatchDatasource(customInstanceSettings, templateService, timeSrv); datasource.getVariables = () => ['test']; - datasource.api.describeLogGroups = jest.fn().mockResolvedValue([]); datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); datasource.api.getRegions = jest.fn().mockResolvedValue([]); datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); datasource.api.getMetrics = jest.fn().mockResolvedValue([]); datasource.api.getAccounts = jest.fn().mockResolvedValue([]); - datasource.logsQueryRunner.defaultLogGroups = []; + datasource.api.describeLogGroups = jest.fn().mockResolvedValue([]); const fetchMock = jest.fn().mockReturnValue(of({})); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/api.ts b/public/app/plugins/datasource/cloudwatch/api.ts index 6e1049b49aa..7b7c9578e38 100644 --- a/public/app/plugins/datasource/cloudwatch/api.ts +++ b/public/app/plugins/datasource/cloudwatch/api.ts @@ -62,16 +62,7 @@ export class CloudWatchAPI extends CloudWatchRequest { ); } - async describeLogGroups(params: DescribeLogGroupsRequest) { - return this.memoizedGetRequest('log-groups', { - ...params, - region: this.templateSrv.replace(this.getActualRegion(params.region)), - }); - } - - async describeCrossAccountLogGroups( - params: DescribeLogGroupsRequest - ): Promise>> { + async describeLogGroups(params: DescribeLogGroupsRequest): Promise>> { return this.memoizedGetRequest>>('describe-log-groups', { ...params, region: this.templateSrv.replace(this.getActualRegion(params.region)), diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index 0ac11b6c51a..d2579d9fc21 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -1,31 +1,21 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import selectEvent from 'react-select-event'; import { AwsAuthType } from '@grafana/aws-sdk'; -import { toOption } from '@grafana/data'; import { configureStore } from 'app/store/configureStore'; -import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; +import { CloudWatchSettings, setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; +import { CloudWatchDatasource } from '../datasource'; import { ConfigEditor, Props } from './ConfigEditor'; +const datasource = new CloudWatchDatasource(CloudWatchSettings); +const loadDataSourceMock = jest.fn(); jest.mock('app/features/plugins/datasource_srv', () => ({ getDatasourceSrv: () => ({ - loadDatasource: jest.fn().mockResolvedValue({ - api: { - describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar'].map(toOption)), - getRegions: jest.fn().mockResolvedValue([ - { - label: 'ap-east-1', - value: 'ap-east-1', - }, - ]), - }, - getActualRegion: jest.fn().mockReturnValue('ap-east-1'), - getVariables: jest.fn().mockReturnValue([]), - }), + loadDatasource: loadDataSourceMock, }), })); @@ -34,10 +24,12 @@ jest.mock('./XrayLinkConfig', () => ({ })); const putMock = jest.fn(); +const getMock = jest.fn(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ put: putMock, + get: getMock, }), })); @@ -59,6 +51,7 @@ const props: Props = { isDefault: true, readOnly: false, withCredentials: false, + version: 2, secureJsonFields: { accessKey: false, secretKey: false, @@ -104,53 +97,184 @@ describe('Render', () => { }; jest.resetAllMocks(); putMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); + getMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); + loadDataSourceMock.mockResolvedValue(datasource); + datasource.api.getRegions = jest.fn().mockResolvedValue([ + { + label: 'ap-east-1', + value: 'ap-east-1', + }, + ]); + datasource.getActualRegion = jest.fn().mockReturnValue('ap-east-1'); + datasource.getVariables = jest.fn().mockReturnValue([]); }); - it('should render component without blowing up', () => { - expect(() => setup()).not.toThrow(); - }); - - it('should disable access key id field', () => { + it('it should disable access key id field when the datasource has been previously configured', async () => { setup({ secureJsonFields: { secretKey: true, }, }); - expect(screen.getByPlaceholderText('Configured')).toBeDisabled(); + await waitFor(async () => expect(screen.getByPlaceholderText('Configured')).toBeDisabled()); }); - it('should show credentials profile name field', () => { + it('should show credentials profile name field', async () => { setup({ jsonData: { authType: AwsAuthType.Credentials, }, }); - expect(screen.getByLabelText('Credentials Profile Name')).toBeInTheDocument(); + await waitFor(async () => expect(screen.getByLabelText('Credentials Profile Name')).toBeInTheDocument()); }); - it('should show access key and secret access key fields', () => { + it('should show access key and secret access key fields when the datasource has not been configured before', async () => { setup({ jsonData: { authType: AwsAuthType.Keys, }, }); - expect(screen.getByLabelText('Access Key ID')).toBeInTheDocument(); - expect(screen.getByLabelText('Secret Access Key')).toBeInTheDocument(); + await waitFor(async () => { + expect(screen.getByLabelText('Access Key ID')).toBeInTheDocument(); + expect(screen.getByLabelText('Secret Access Key')).toBeInTheDocument(); + }); }); - it('should show arn role field', () => { + it('should show arn role field', async () => { setup({ jsonData: { authType: AwsAuthType.ARN, }, }); - expect(screen.getByLabelText('Assume Role ARN')).toBeInTheDocument(); + await waitFor(async () => expect(screen.getByLabelText('Assume Role ARN')).toBeInTheDocument()); }); - it('should load log groups when multiselect is opened', async () => { + it('should display log group selector field', async () => { setup(); - const multiselect = await screen.findByLabelText('Log Groups'); - selectEvent.openMenu(multiselect); - expect(await screen.findByText('logGroup-foo')).toBeInTheDocument(); + await waitFor(async () => expect(await screen.getByText('Select Log Groups')).toBeInTheDocument()); + }); + + it('should only display the first two default log groups and show all of them when clicking "Show all" button', async () => { + setup({ + version: 2, + jsonData: { + logGroups: [ + { arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-foo:*', name: 'logGroup-foo' }, + { arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-bar:*', name: 'logGroup-bar' }, + { arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-baz:*', name: 'logGroup-baz' }, + ], + }, + }); + await waitFor(async () => { + expect(await screen.getByText('logGroup-foo')).toBeInTheDocument(); + expect(await screen.getByText('logGroup-bar')).toBeInTheDocument(); + expect(await screen.queryByText('logGroup-baz')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByText('Show all')); + + expect(await screen.getByText('logGroup-baz')).toBeInTheDocument(); + }); + }); + + it('should load the data source if it was saved before', async () => { + const SAVED_VERSION = 2; + const newProps = { + ...props, + options: { + ...props.options, + version: SAVED_VERSION, + }, + }; + + render(); + await waitFor(async () => expect(loadDataSourceMock).toHaveBeenCalled()); + }); + + it('should not load the data source if it wasnt saved before', async () => { + const SAVED_VERSION = undefined; + const newProps = { + ...props, + options: { + ...props.options, + version: SAVED_VERSION, + }, + }; + + render(); + await waitFor(async () => expect(loadDataSourceMock).not.toHaveBeenCalled()); + }); + + it('should show error message if Select log group button is clicked when data source is never saved', async () => { + const SAVED_VERSION = undefined; + const newProps = { + ...props, + options: { + ...props.options, + version: SAVED_VERSION, + }, + }; + + render(); + + await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); + await userEvent.click(screen.getByText('Select Log Groups')); + await waitFor(() => + expect(screen.getByText('You need to save the data source before adding log groups.')).toBeInTheDocument() + ); + }); + + it('should show error message if Select log group button is clicked when data source is saved before but have unsaved changes', async () => { + const SAVED_VERSION = 3; + const newProps = { + ...props, + options: { + ...props.options, + version: SAVED_VERSION, + }, + }; + const { rerender } = render(); + await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); + const rerenderProps = { + ...newProps, + options: { + ...newProps.options, + jsonData: { + ...newProps.options.jsonData, + authType: AwsAuthType.Default, + }, + }, + }; + rerender(); + await waitFor(() => expect(screen.getByText('AWS SDK Default')).toBeInTheDocument()); + await userEvent.click(screen.getByText('Select Log Groups')); + await waitFor(() => + expect( + screen.getByText( + 'You have unsaved connection detail changes. You need to save the data source before adding log groups.' + ) + ).toBeInTheDocument() + ); + }); + + it('should open log group selector if Select log group button is clicked when data source has saved changes', async () => { + const SAVED_VERSION = undefined; + const newProps = { + ...props, + options: { + ...props.options, + version: SAVED_VERSION, + }, + }; + const { rerender } = render(); + await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); + const rerenderProps = { + ...newProps, + options: { + ...newProps.options, + version: 1, + }, + }; + rerender(); + await userEvent.click(screen.getByText('Select Log Groups')); + await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument()); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index b5abe896a07..e13ce093c1d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -7,10 +7,8 @@ import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOption, updateDatasourcePluginJsonDataOption, - updateDatasourcePluginOption, } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; -import { Input, InlineField } from '@grafana/ui'; +import { Input, InlineField, FieldProps } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createWarningNotification } from 'app/core/copy/appNotification'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -20,43 +18,24 @@ import { SelectableResourceValue } from '../api'; import { CloudWatchDatasource } from '../datasource'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types'; -import { LogGroupSelector } from './LogGroupSelector'; +import { LogGroupsField } from './LogGroups/LogGroupsField'; import { XrayLinkConfig } from './XrayLinkConfig'; export type Props = DataSourcePluginOptionsEditorProps; -export const ConfigEditor: FC = (props: Props) => { - const { options } = props; - const { defaultLogGroups, logsTimeout, defaultRegion } = options.jsonData; - const [saved, setSaved] = useState(!!options.version && options.version > 1); +type LogGroupFieldState = Pick & { error?: string | null }; - const datasource = useDatasource(options.name, saved); +export const ConfigEditor: FC = (props: Props) => { + const { options, onOptionsChange } = props; + const { defaultLogGroups, logsTimeout, defaultRegion, logGroups } = options.jsonData; + const datasource = useDatasource(props); useAuthenticationWarning(options.jsonData); const logsTimeoutError = useTimoutValidation(logsTimeout); - useEffect(() => { - setSaved(false); - }, [ - props.options.jsonData.assumeRoleArn, - props.options.jsonData.authType, - props.options.jsonData.defaultRegion, - props.options.jsonData.endpoint, - props.options.jsonData.externalId, - props.options.jsonData.profile, - props.options.secureJsonData?.accessKey, - props.options.secureJsonData?.secretKey, - ]); - - const saveOptions = async (): Promise => { - if (saved) { - return; - } - await getBackendSrv() - .put(`/api/datasources/${options.id}`, options) - .then((result: { datasource: any }) => { - updateDatasourcePluginOption(props, 'version', result.datasource.version); - }); - setSaved(true); - }; + const saved = useDataSourceSavedState(props); + const [logGroupFieldState, setLogGroupFieldState] = useState({ + invalid: false, + }); + useEffect(() => setLogGroupFieldState({ invalid: false }), [props.options]); return ( <> @@ -107,17 +86,41 @@ export const ConfigEditor: FC = (props: Props) => { label="Default Log Groups" labelWidth={28} tooltip="Optionally, specify default log groups for CloudWatch Logs queries." + shrink={true} + {...logGroupFieldState} > - { - updateDatasourcePluginJsonDataOption(props, 'defaultLogGroups', logGroups); + onBeforeOpen={() => { + if (saved) { + return; + } + + let error = 'You need to save the data source before adding log groups.'; + if (props.options.version && props.options.version > 1) { + error = + 'You have unsaved connection detail changes. You need to save the data source before adding log groups.'; + } + setLogGroupFieldState({ + invalid: true, + error, + }); + throw new Error(error); }} - onOpenMenu={saveOptions} - width={60} - saved={saved} + legacyLogGroupNames={defaultLogGroups} + logGroups={logGroups} + onChange={(updatedLogGroups) => { + onOptionsChange({ + ...props.options, + jsonData: { + ...props.options.jsonData, + logGroups: updatedLogGroups, + defaultLogGroups: undefined, + }, + }); + }} + maxNoOfVisibleLogGroups={2} /> @@ -148,22 +151,20 @@ function useAuthenticationWarning(jsonData: CloudWatchJsonData) { }, [jsonData.authType, jsonData.database, jsonData.profile]); } -function useDatasource(datasourceName: string, saved: boolean) { +function useDatasource(props: Props) { const [datasource, setDatasource] = useState(); useEffect(() => { - // reload the datasource when it's saved - if (!saved) { - return; + if (props.options.version) { + getDatasourceSrv() + .loadDatasource(props.options.name) + .then((datasource) => { + if (datasource instanceof CloudWatchDatasource) { + setDatasource(datasource); + } + }); } - getDatasourceSrv() - .loadDatasource(datasourceName) - .then((datasource) => { - // It's really difficult to type .loadDatasource() because it's inherently untyped as it involves two JSON.parse()'s - // So a "as" type assertion here is a necessary evil. - setDatasource(datasource as CloudWatchDatasource); - }); - }, [datasourceName, saved]); + }, [props.options.version, props.options.name]); return datasource; } @@ -190,3 +191,25 @@ function useTimoutValidation(value: string | undefined) { ); return err; } + +function useDataSourceSavedState(props: Props) { + const [saved, setSaved] = useState(!!props.options.version && props.options.version > 1); + useEffect(() => { + setSaved(false); + }, [ + props.options.jsonData.assumeRoleArn, + props.options.jsonData.authType, + props.options.jsonData.defaultRegion, + props.options.jsonData.endpoint, + props.options.jsonData.externalId, + props.options.jsonData.profile, + props.options.secureJsonData?.accessKey, + props.options.secureJsonData?.secretKey, + ]); + + useEffect(() => { + props.options.version && setSaved(true); + }, [props.options.version]); + + return saved; +} diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx deleted file mode 100644 index d68dc464452..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -// eslint-disable-next-line lodash/import-scope -import lodash from 'lodash'; -import React from 'react'; - -import { config } from '@grafana/runtime'; - -import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; -import { CloudWatchLogsQuery } from '../types'; - -import { LogGroupSelection } from './LogGroupSelection'; - -const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQuerying; -const originalDebounce = lodash.debounce; - -const defaultProps = { - datasource: setupMockedDataSource().datasource, - query: { - queryMode: 'Logs', - id: '', - region: '', - refId: '', - } as CloudWatchLogsQuery, - onChange: jest.fn(), -}; -describe('LogGroupSelection', () => { - beforeEach(() => { - lodash.debounce = jest.fn().mockImplementation((fn) => { - fn.cancel = () => {}; - return fn; - }); - }); - afterEach(() => { - config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; - lodash.debounce = originalDebounce; - }); - it('renders the old logGroupSelector when the feature toggle is disabled and there are no linked accounts', async () => { - config.featureToggles.cloudWatchCrossAccountQuerying = false; - render(); - await waitFor(() => screen.getByText('Choose Log Groups')); - expect(screen.queryByText('Select Log Groups')).not.toBeInTheDocument(); - }); - it('renders the old logGroupSelector when the feature toggle is disabled but there are linked accounts', async () => { - config.featureToggles.cloudWatchCrossAccountQuerying = false; - const ds = setupMockedDataSource().datasource; - ds.api.getAccounts = () => - Promise.resolve([ - { - arn: 'arn', - id: 'accountId', - label: 'label', - isMonitoringAccount: true, - }, - ]); - - render(); - await waitFor(() => screen.getByText('Choose Log Groups')); - expect(screen.queryByText('Select Log Groups')).not.toBeInTheDocument(); - }); - - it('renders the old logGroupSelector when the feature toggle is enabled but there are no linked accounts', async () => { - config.featureToggles.cloudWatchCrossAccountQuerying = true; - render(); - await waitFor(() => screen.getByText('Choose Log Groups')); - expect(screen.queryByText('Select Log Groups')).not.toBeInTheDocument(); - }); - - it('renders the new logGroupSelector when the feature toggle is enabled and there are linked accounts', async () => { - config.featureToggles.cloudWatchCrossAccountQuerying = true; - const ds = setupMockedDataSource().datasource; - ds.api.getAccounts = () => - Promise.resolve([ - { - arn: 'arn', - id: 'accountId', - label: 'label', - isMonitoringAccount: true, - }, - ]); - - render(); - await waitFor(() => screen.getByText('Select Log Groups')); - expect(screen.queryByText('Choose Log Groups')).not.toBeInTheDocument(); - }); -}); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx deleted file mode 100644 index ec22e62d809..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { config } from '@grafana/runtime'; -import { LegacyForms } from '@grafana/ui'; - -import { CloudWatchDatasource } from '../datasource'; -import { useAccountOptions } from '../hooks'; -import { CloudWatchLogsQuery, CloudWatchQuery, DescribeLogGroupsRequest, LogGroup } from '../types'; - -import { CrossAccountLogsQueryField } from './CrossAccountLogsQueryField'; -import { LogGroupSelector } from './LogGroupSelector'; - -type Props = { - datasource: CloudWatchDatasource; - query: CloudWatchLogsQuery; - onChange: (value: CloudWatchQuery) => void; -}; - -const rowGap = css` - gap: 3px; -`; - -export const LogGroupSelection = ({ datasource, query, onChange }: Props) => { - const accountState = useAccountOptions(datasource.api, query.region); - - return ( -
- {config.featureToggles.cloudWatchCrossAccountQuerying && accountState?.value?.length ? ( - ) => - datasource.api.describeCrossAccountLogGroups({ region: query.region, ...params }) - } - onChange={(selectedLogGroups: LogGroup[]) => { - onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] }); - }} - accountOptions={accountState.value} - selectedLogGroups={query.logGroups ?? []} /* todo handle defaults */ - /> - ) : ( - - } - /> - )} -
- ); -}; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx deleted file mode 100644 index 22d5b36464b..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import lodash from 'lodash'; // eslint-disable-line lodash/import-scope -import React from 'react'; -import { openMenu, select } from 'react-select-event'; - -import { toOption } from '@grafana/data'; - -import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; -import { DescribeLogGroupsRequest } from '../types'; - -import { LogGroupSelector, LogGroupSelectorProps } from './LogGroupSelector'; - -const ds = setupMockedDataSource(); - -describe('LogGroupSelector', () => { - const onChange = jest.fn(); - const defaultProps: LogGroupSelectorProps = { - region: 'region1', - datasource: ds.datasource, - selectedLogGroups: [], - onChange, - }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('does not clear previously selected log groups after region change', async () => { - ds.datasource.api.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => { - if (params.region === 'region1') { - return Promise.resolve(['log_group_1'].map(toOption)); - } else { - return Promise.resolve(['log_group_2'].map(toOption)); - } - }); - const props = { - ...defaultProps, - selectedLogGroups: ['log_group_1'], - }; - - const { rerender } = render(); - expect(await screen.findByText('log_group_1')).toBeInTheDocument(); - - act(() => rerender()); - expect(await screen.findByText('log_group_1')).toBeInTheDocument(); - }); - - it('should merge results of remote log groups search with existing results', async () => { - lodash.debounce = jest.fn().mockImplementation((fn) => fn); - const allLogGroups = [ - 'AmazingGroup', - 'AmazingGroup2', - 'AmazingGroup3', - 'BeautifulGroup', - 'BeautifulGroup2', - 'BeautifulGroup3', - 'CrazyGroup', - 'CrazyGroup2', - 'CrazyGroup3', - 'DeliciousGroup', - 'DeliciousGroup2', - 'DeliciousGroup3', - 'VelvetGroup', - 'VelvetGroup2', - 'VelvetGroup3', - 'WaterGroup', - 'WaterGroup2', - 'WaterGroup3', - ]; - const testLimit = 10; - - ds.datasource.api.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => { - const theLogGroups = allLogGroups - .filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? '')) - .slice(0, Math.max(params.limit ?? testLimit, testLimit)); - return Promise.resolve(theLogGroups.map(toOption)); - }); - const props = { - ...defaultProps, - }; - render(); - const multiselect = await screen.findByLabelText('Log Groups'); - - // Adds the 3 Water groups to the 10 loaded in initially - await userEvent.type(multiselect, 'Water'); - // The 3 Water groups + the create option - expect(screen.getAllByLabelText('Select option').length).toBe(4); - await userEvent.clear(multiselect); - expect(screen.getAllByLabelText('Select option').length).toBe(testLimit + 3); - - // Adds the three Velvet groups to the previous 13 - await userEvent.type(multiselect, 'Velv'); - // The 3 Velvet groups + the create option - expect(screen.getAllByLabelText('Select option').length).toBe(4); - await userEvent.clear(multiselect); - expect(screen.getAllByLabelText('Select option').length).toBe(testLimit + 6); - }); - - it('should render template variables a selectable option', async () => { - lodash.debounce = jest.fn().mockImplementation((fn) => fn); - ds.datasource.api.describeLogGroups = jest.fn().mockResolvedValue([]); - const onChange = jest.fn(); - const props = { - ...defaultProps, - onChange, - }; - render(); - - const logGroupSelector = await screen.findByLabelText('Log Groups'); - expect(logGroupSelector).toBeInTheDocument(); - - await openMenu(logGroupSelector); - const templateVariableSelector = await screen.findByText('Template Variables'); - expect(templateVariableSelector).toBeInTheDocument(); - - userEvent.click(templateVariableSelector); - await select(await screen.findByLabelText('Select option'), 'test'); - - expect(onChange).toBeCalledWith(['test']); - }); -}); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx deleted file mode 100644 index 29013b78047..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { debounce, unionBy } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { SelectableValue, toOption } from '@grafana/data'; -import { MultiSelect } from '@grafana/ui'; -import { InputActionMeta } from '@grafana/ui/src/components/Select/types'; -import { notifyApp } from 'app/core/actions'; -import { createErrorNotification } from 'app/core/copy/appNotification'; -import { dispatch } from 'app/store/store'; - -import { CloudWatchDatasource } from '../datasource'; -import { appendTemplateVariables } from '../utils/utils'; - -const MAX_LOG_GROUPS = 20; -const MAX_VISIBLE_LOG_GROUPS = 4; -const DEBOUNCE_TIMER = 300; - -export interface LogGroupSelectorProps { - region: string; - selectedLogGroups: string[]; - onChange: (logGroups: string[]) => void; - - datasource?: CloudWatchDatasource; - onOpenMenu?: () => Promise; - refId?: string; - width?: number | 'auto'; - saved?: boolean; // is only used in the config editor -} - -export const LogGroupSelector: React.FC = ({ - region, - selectedLogGroups, - onChange, - datasource, - onOpenMenu, - refId, - width, - saved = true, -}) => { - const [loadingLogGroups, setLoadingLogGroups] = useState(false); - const [availableLogGroups, setAvailableLogGroups] = useState>>([]); - const logGroupOptions = useMemo( - () => unionBy(availableLogGroups, selectedLogGroups?.map(toOption), 'value'), - [availableLogGroups, selectedLogGroups] - ); - - const fetchLogGroupOptions = useCallback( - async (region: string, logGroupNamePrefix?: string) => { - if (!datasource) { - return []; - } - try { - const logGroups = await datasource.api.describeLogGroups({ - refId, - region, - logGroupNamePrefix, - }); - return logGroups; - } catch (err) { - dispatch(notifyApp(createErrorNotification(typeof err === 'string' ? err : JSON.stringify(err)))); - return []; - } - }, - [datasource, refId] - ); - - const onLogGroupSearch = async (searchTerm: string, region: string, actionMeta: InputActionMeta) => { - if (actionMeta.action !== 'input-change' || !datasource) { - return; - } - - // No need to fetch matching log groups if the search term isn't valid - // This is also useful for preventing searches when a user is typing out a log group with template vars - // See https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_LogGroup.html for the source of the pattern below - const logGroupNamePattern = /^[\.\-_/#A-Za-z0-9]+$/; - if (!logGroupNamePattern.test(searchTerm)) { - if (searchTerm !== '') { - dispatch(notifyApp(createErrorNotification('Invalid Log Group name: ' + searchTerm))); - } - return; - } - - setLoadingLogGroups(true); - const matchingLogGroups = await fetchLogGroupOptions(region, searchTerm); - setAvailableLogGroups(unionBy(availableLogGroups, matchingLogGroups, 'value')); - setLoadingLogGroups(false); - }; - - // Reset the log group options if the datasource or region change and are saved - useEffect(() => { - async function getAvailableLogGroupOptions() { - // Don't call describeLogGroups if datasource or region is undefined - if (!datasource || !datasource.getActualRegion(region)) { - setAvailableLogGroups([]); - return; - } - - setLoadingLogGroups(true); - return fetchLogGroupOptions(datasource.getActualRegion(region)) - .then((logGroups) => { - setAvailableLogGroups(logGroups); - }) - .finally(() => { - setLoadingLogGroups(false); - }); - } - - // Config editor does not fetch new log group options unless changes have been saved - saved && getAvailableLogGroupOptions(); - - // if component unmounts in the middle of setting state, we reset state and unsubscribe from fetchLogGroupOptions - return () => { - setAvailableLogGroups([]); - setLoadingLogGroups(false); - }; - // this hook shouldn't get called every time selectedLogGroups or onChange updates - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasource, region, saved]); - - const onOpenLogGroupMenu = async () => { - if (onOpenMenu) { - await onOpenMenu(); - } - }; - - const onLogGroupSearchDebounced = debounce(onLogGroupSearch, DEBOUNCE_TIMER); - - return ( - onChange(v.filter(({ value }) => value).map(({ value }) => value))} - closeMenuOnSelect={false} - isClearable - isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS} - placeholder="Choose Log Groups" - maxVisibleValues={MAX_VISIBLE_LOG_GROUPS} - noOptionsMessage="No log groups available" - isLoading={loadingLogGroups} - onOpenMenu={onOpenLogGroupMenu} - onInputChange={(value, actionMeta) => { - onLogGroupSearchDebounced(value, region, actionMeta); - }} - width={width} - /> - ); -}; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx new file mode 100644 index 00000000000..f15b864520b --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx @@ -0,0 +1,58 @@ +import { render, screen, waitFor } from '@testing-library/react'; +// eslint-disable-next-line lodash/import-scope +import lodash from 'lodash'; +import React from 'react'; + +import { config } from '@grafana/runtime'; + +import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; + +import { LogGroupsField } from './LogGroupsField'; + +const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQuerying; +const originalDebounce = lodash.debounce; + +const defaultProps = { + datasource: setupMockedDataSource().datasource, + region: '', + onChange: jest.fn(), +}; +describe('LogGroupSelection', () => { + beforeEach(() => { + jest.resetAllMocks(); + lodash.debounce = jest.fn().mockImplementation((fn) => { + fn.cancel = () => {}; + return fn; + }); + }); + afterEach(() => { + config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; + lodash.debounce = originalDebounce; + }); + + it('call describeCrossAccountLogGroups to get associated log group arns and then update props if rendered with legacy log group names', async () => { + config.featureToggles.cloudWatchCrossAccountQuerying = true; + defaultProps.datasource.api.describeLogGroups = jest + .fn() + .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); + render(); + + await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); + expect(defaultProps.datasource.api.describeLogGroups).toHaveBeenCalledWith({ + region: defaultProps.region, + logGroupNamePrefix: 'loggroupname', + }); + expect(defaultProps.onChange).toHaveBeenCalledWith([{ arn: 'arn', name: 'loggroupname' }]); + }); + + it('should not call describeCrossAccountLogGroups and update props if rendered with log groups', async () => { + config.featureToggles.cloudWatchCrossAccountQuerying = true; + defaultProps.datasource.api.describeLogGroups = jest + .fn() + .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); + render(); + await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); + expect(defaultProps.datasource.api.describeLogGroups).not.toHaveBeenCalled(); + expect(defaultProps.onChange).not.toHaveBeenCalled(); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx new file mode 100644 index 00000000000..3964a48fe3b --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx @@ -0,0 +1,77 @@ +import { css } from '@emotion/css'; +import React, { useEffect, useState } from 'react'; + +import { CloudWatchDatasource } from '../../datasource'; +import { useAccountOptions } from '../../hooks'; +import { DescribeLogGroupsRequest, LogGroup } from '../../types'; + +import { LogGroupsSelector } from './LogGroupsSelector'; +import { SelectedLogGroups } from './SelectedLogGroups'; + +type Props = { + datasource?: CloudWatchDatasource; + onChange: (logGroups: LogGroup[]) => void; + legacyLogGroupNames?: string[]; + logGroups?: LogGroup[]; + region: string; + maxNoOfVisibleLogGroups?: number; + onBeforeOpen?: () => void; +}; + +const rowGap = css` + gap: 3px; +`; + +export const LogGroupsField = ({ + datasource, + onChange, + legacyLogGroupNames, + logGroups, + region, + maxNoOfVisibleLogGroups, + onBeforeOpen, +}: Props) => { + const accountState = useAccountOptions(datasource?.api, region); + const [loadingLogGroupsStarted, setLoadingLogGroupsStarted] = useState(false); + + useEffect(() => { + // If log group names are stored in the query model, make a new DescribeLogGroups request for each log group to load the arn. Then update the query model. + if (datasource && !loadingLogGroupsStarted && !logGroups?.length && legacyLogGroupNames?.length) { + setLoadingLogGroupsStarted(true); + Promise.all( + legacyLogGroupNames.map((lg) => datasource.api.describeLogGroups({ region: region, logGroupNamePrefix: lg })) + ) + .then((results) => { + const a = results.flatMap((r) => + r.map((lg) => ({ + arn: lg.value.arn, + name: lg.value.name, + accountId: lg.accountId, + })) + ); + + onChange(a); + }) + .catch(console.error); + } + }, [datasource, legacyLogGroupNames, logGroups, onChange, region, loadingLogGroupsStarted]); + + return ( +
+ ) => + datasource?.api.describeLogGroups({ region: region, ...params }) ?? [] + } + onChange={onChange} + accountOptions={accountState.value} + selectedLogGroups={logGroups} + onBeforeOpen={onBeforeOpen} + /> + +
+ ); +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx similarity index 84% rename from public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx rename to public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx index bb393f64682..7897f84e3d8 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx @@ -5,9 +5,9 @@ import lodash from 'lodash'; import React from 'react'; import selectEvent from 'react-select-event'; -import { ResourceResponse, LogGroupResponse } from '../types'; +import { ResourceResponse, LogGroupResponse } from '../../types'; -import { CrossAccountLogsQueryField } from './CrossAccountLogsQueryField'; +import { LogGroupsSelector } from './LogGroupsSelector'; const defaultProps = { selectedLogGroups: [], @@ -68,9 +68,9 @@ describe('CrossAccountLogsQueryField', () => { lodash.debounce = originalDebounce; }); it('opens a modal with a search field when the Select Log Groups Button is clicked', async () => { - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); + expect(screen.getByText('Log group name prefix')).toBeInTheDocument(); }); it('calls fetchLogGroups the first time the modal opens and renders a loading widget and then a checkbox for every log group', async () => { @@ -79,7 +79,7 @@ describe('CrossAccountLogsQueryField', () => { await Promise.all([defer.promise]); return defaultProps.fetchLogGroups(); }); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); expect(screen.getByText('Loading...')).toBeInTheDocument(); defer.resolve(); @@ -94,7 +94,7 @@ describe('CrossAccountLogsQueryField', () => { await Promise.all([defer.promise]); return []; }); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); expect(screen.getByText('Loading...')).toBeInTheDocument(); defer.resolve(); @@ -106,9 +106,9 @@ describe('CrossAccountLogsQueryField', () => { it('calls fetchLogGroups with a search phrase when it is typed in the Search Field', async () => { const fetchLogGroups = jest.fn(() => defaultProps.fetchLogGroups()); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); + expect(screen.getByText('Log group name prefix')).toBeInTheDocument(); await userEvent.type(screen.getByLabelText('log group search'), 'something'); await waitFor(() => screen.getByDisplayValue('something')); expect(fetchLogGroups).toBeCalledWith({ accountId: 'all', logGroupPattern: 'something' }); @@ -127,7 +127,7 @@ describe('CrossAccountLogsQueryField', () => { once = true; return defaultProps.fetchLogGroups(); }); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); expect(screen.getByText('Loading...')).toBeInTheDocument(); firstCall.resolve(); @@ -145,18 +145,18 @@ describe('CrossAccountLogsQueryField', () => { it('shows a log group as checked after the user checks it', async () => { const onChange = jest.fn(); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); + expect(screen.getByText('Log group name prefix')).toBeInTheDocument(); expect(screen.getByLabelText('logGroup2')).not.toBeChecked(); await userEvent.click(screen.getByLabelText('logGroup2')); expect(screen.getByLabelText('logGroup2')).toBeChecked(); }); it('calls onChange with the selected log group when checked and the user clicks the Add button', async () => { const onChange = jest.fn(); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); + expect(screen.getByText('Log group name prefix')).toBeInTheDocument(); await userEvent.click(screen.getByLabelText('logGroup2')); await userEvent.click(screen.getByText('Add log groups')); expect(onChange).toHaveBeenCalledWith([ @@ -171,9 +171,9 @@ describe('CrossAccountLogsQueryField', () => { it('does not call onChange after a selection if the user hits the cancel button', async () => { const onChange = jest.fn(); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); + expect(screen.getByText('Log group name prefix')).toBeInTheDocument(); await userEvent.click(screen.getByLabelText('logGroup2')); await userEvent.click(screen.getByText('Cancel')); expect(onChange).not.toHaveBeenCalledWith([ @@ -194,7 +194,7 @@ describe('CrossAccountLogsQueryField', () => { await Promise.all([defer.promise]); return []; }); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); expect(screen.queryByText(labelText)).not.toBeInTheDocument(); defer.resolve(); @@ -212,7 +212,7 @@ describe('CrossAccountLogsQueryField', () => { }, })); }); - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); expect(screen.queryByText(labelText)).not.toBeInTheDocument(); defer.resolve(); @@ -220,7 +220,7 @@ describe('CrossAccountLogsQueryField', () => { }); it('should display log groups counter label', async () => { - render(); + render(); await userEvent.click(screen.getByText('Select Log Groups')); await waitFor(() => expect(screen.getByText('0 log groups selected')).toBeInTheDocument()); await userEvent.click(screen.getByLabelText('logGroup2')); diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx similarity index 82% rename from public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx rename to public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx index 83ef392e84b..5ebcb1b8163 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx @@ -1,54 +1,64 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { EditorField, Space } from '@grafana/experimental'; import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui'; -import Search from '../Search'; -import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../types'; - -import { Account, ALL_ACCOUNTS_OPTION } from './Account'; -import { SelectedLogsGroups } from './SelectedLogsGroups'; -import getStyles from './styles'; +import Search from '../../Search'; +import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../../types'; +import { Account, ALL_ACCOUNTS_OPTION } from '../Account'; +import getStyles from '../styles'; type CrossAccountLogsQueryProps = { - selectedLogGroups: LogGroup[]; - accountOptions: Array>; + selectedLogGroups?: LogGroup[]; + accountOptions?: Array>; fetchLogGroups: (params: Partial) => Promise>>; onChange: (selectedLogGroups: LogGroup[]) => void; + onBeforeOpen?: () => void; }; -export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => { +export const LogGroupsSelector = ({ + accountOptions = [], + fetchLogGroups, + onChange, + onBeforeOpen, + ...props +}: CrossAccountLogsQueryProps) => { const [isModalOpen, setIsModalOpen] = useState(false); const [selectableLogGroups, setSelectableLogGroups] = useState([]); - const [selectedLogGroups, setSelectedLogGroups] = useState(props.selectedLogGroups); + const [selectedLogGroups, setSelectedLogGroups] = useState(props.selectedLogGroups ?? []); const [searchPhrase, setSearchPhrase] = useState(''); const [searchAccountId, setSearchAccountId] = useState(ALL_ACCOUNTS_OPTION.value); const [isLoading, setIsLoading] = useState(false); const styles = useStyles2(getStyles); + + useEffect(() => { + setSelectedLogGroups(props.selectedLogGroups ?? []); + }, [props.selectedLogGroups]); + const toggleModal = () => { setIsModalOpen(!isModalOpen); if (isModalOpen) { } else { - setSelectedLogGroups(props.selectedLogGroups); + setSelectedLogGroups(selectedLogGroups); searchFn(searchPhrase, searchAccountId); } }; const accountNameById = useMemo(() => { const idsToNames: Record = {}; - props.accountOptions.forEach((a) => { + accountOptions.forEach((a) => { if (a.value && a.label) { idsToNames[a.value] = a.label; } }); return idsToNames; - }, [props.accountOptions]); + }, [accountOptions]); const searchFn = async (searchTerm?: string, accountId?: string) => { setIsLoading(true); try { - const possibleLogGroups = await props.fetchLogGroups({ + const possibleLogGroups = await fetchLogGroups({ logGroupPattern: searchTerm, accountId: accountId, }); @@ -75,12 +85,12 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => }; const handleApply = () => { - props.onChange(selectedLogGroups); + onChange(selectedLogGroups); toggleModal(); }; const handleCancel = () => { - setSelectedLogGroups(props.selectedLogGroups); + setSelectedLogGroups(selectedLogGroups); toggleModal(); }; @@ -89,7 +99,7 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
- + { searchFn(phrase, searchAccountId); @@ -105,7 +115,7 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => searchFn(searchPhrase, accountId); setSearchAccountId(accountId || ALL_ACCOUNTS_OPTION.value); }} - accountOptions={props.accountOptions} + accountOptions={accountOptions} accountId={searchAccountId} />
@@ -154,7 +164,7 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => value={!!(row.arn && selectedLogGroups.some((lg) => lg.arn === row.arn))} /> -
@@ -183,12 +193,19 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
-
- - ); }; diff --git a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.test.tsx similarity index 79% rename from public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx rename to public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.test.tsx index d414fc52d2e..43b9830fb76 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.test.tsx @@ -2,9 +2,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { LogGroup } from '../types'; +import { LogGroup } from '../../types'; -import { SelectedLogsGroups } from './SelectedLogsGroups'; +import { SelectedLogGroups } from './SelectedLogGroups'; const selectedLogGroups: LogGroup[] = [ { @@ -27,11 +27,11 @@ describe('SelectedLogsGroups', () => { }); describe("'Show more' button", () => { it('should not be displayed in case 0 logs have been selected', async () => { - render(); + render(); await waitFor(() => expect(screen.queryByText('Show all')).not.toBeInTheDocument()); }); it('should not be displayed in case logs group have been selected but theyre less than 10', async () => { - render(); + render(); await waitFor(() => expect(screen.queryByText('Show all')).not.toBeInTheDocument()); }); it('should be displayed in case more than 10 log groups have been selected', async () => { @@ -39,14 +39,14 @@ describe('SelectedLogsGroups', () => { arn: `logGroup${i}`, name: `logGroup${i}`, })); - render(); + render(); await waitFor(() => expect(screen.getByText('Show all')).toBeInTheDocument()); }); }); describe("'Clear selection' button", () => { it('should not be displayed in case 0 logs have been selected', async () => { - render(); + render(); await waitFor(() => expect(screen.queryByText('Clear selection')).not.toBeInTheDocument()); }); it('should be displayed in case at least one log group have been selected', async () => { @@ -54,7 +54,7 @@ describe('SelectedLogsGroups', () => { arn: `logGroup${i}`, name: `logGroup${i}`, })); - render(); + render(); await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument()); }); @@ -63,7 +63,7 @@ describe('SelectedLogsGroups', () => { arn: `logGroup${i}`, name: `logGroup${i}`, })); - render(); + render(); await waitFor(() => userEvent.click(screen.getByText('Clear selection'))); await waitFor(() => expect(screen.getByText('Are you sure you want to clear all log groups?')).toBeInTheDocument() @@ -75,7 +75,7 @@ describe('SelectedLogsGroups', () => { describe("'Clear selection' button", () => { it('should not be displayed in case 0 logs have been selected', async () => { - render(); + render(); await waitFor(() => expect(screen.queryByText('Clear selection')).not.toBeInTheDocument()); }); it('should be displayed in case at least one log group have been selected', async () => { @@ -83,7 +83,7 @@ describe('SelectedLogsGroups', () => { arn: `logGroup${i}`, name: `logGroup${i}`, })); - render(); + render(); await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument()); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.tsx similarity index 78% rename from public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx rename to public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.tsx index e4ab695865b..37659e4880d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/SelectedLogGroups.tsx @@ -2,27 +2,31 @@ import React, { useEffect, useState } from 'react'; import { Button, ConfirmModal, useStyles2 } from '@grafana/ui'; -import { LogGroup } from '../types'; - -import getStyles from './styles'; +import { LogGroup } from '../../types'; +import getStyles from '../styles'; type CrossAccountLogsQueryProps = { - selectedLogGroups: LogGroup[]; + selectedLogGroups?: LogGroup[]; onChange: (selectedLogGroups: LogGroup[]) => void; + maxNoOfVisibleLogGroups?: number; }; -const MAX_VISIBLE_LOG_GROUPS = 6; +const MAX_NO_OF_VISIBLE_LOG_GROUPS = 6; -export const SelectedLogsGroups = ({ selectedLogGroups, onChange }: CrossAccountLogsQueryProps) => { +export const SelectedLogGroups = ({ + selectedLogGroups = [], + onChange, + maxNoOfVisibleLogGroups = MAX_NO_OF_VISIBLE_LOG_GROUPS, +}: CrossAccountLogsQueryProps) => { const styles = useStyles2(getStyles); const [showConfirm, setShowConfirm] = useState(false); const [visibleSelectecLogGroups, setVisibleSelectecLogGroups] = useState( - selectedLogGroups.slice(0, MAX_VISIBLE_LOG_GROUPS) + selectedLogGroups.slice(0, MAX_NO_OF_VISIBLE_LOG_GROUPS) ); useEffect(() => { - setVisibleSelectecLogGroups(selectedLogGroups.slice(0, MAX_VISIBLE_LOG_GROUPS)); - }, [selectedLogGroups]); + setVisibleSelectecLogGroups(selectedLogGroups.slice(0, maxNoOfVisibleLogGroups)); + }, [selectedLogGroups, maxNoOfVisibleLogGroups]); return ( <> diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx deleted file mode 100644 index 95fdb2f42d8..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope -import React from 'react'; - -import { ExploreId } from '../../../../types'; -import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; - -import CloudWatchLogsQueryField from './LogsQueryField'; - -jest - .spyOn(_, 'debounce') - .mockImplementation((func: (...args: any) => any, wait?: number) => func as DebouncedFunc); - -describe('CloudWatchLogsQueryField', () => { - it('loads defaultLogGroups', async () => { - const onRunQuery = jest.fn(); - const ds = setupMockedDataSource(); - ds.datasource.logsQueryRunner.defaultLogGroups = ['foo']; - - render( - {}} - /> - ); - - await waitFor(() => { - expect(screen.getByText('foo')).toBeInTheDocument(); - }); - }); -}); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index b1f09053028..407afce7100 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -23,7 +23,7 @@ import syntax from '../syntax'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types'; import { getStatsGroups } from '../utils/query/getStatsGroups'; -import { LogGroupSelection } from './LogGroupSelection'; +import { LogGroupsField } from './LogGroups/LogGroupsField'; export interface CloudWatchLogsQueryFieldProps extends QueryEditorProps, @@ -84,7 +84,15 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) = return ( <> - + { + onChange({ ...query, logGroups, logGroupNames: undefined }); + }} + />
{ }); describe('when using grafana 7.0.0 style logs query', () => { - it('should render the metrics query editor', async () => { + it('should render the logs query editor', async () => { const query = { ...migratedFields, alias: '', @@ -123,7 +123,7 @@ describe('QueryEditor should render right editor', () => { await act(async () => { render(); }); - expect(screen.getByText('Log Groups')).toBeInTheDocument(); + expect(screen.getByText('Select Log Groups')).toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/styles.ts b/public/app/plugins/datasource/cloudwatch/components/styles.ts index 7aebe20cce2..6a5da27adde 100644 --- a/public/app/plugins/datasource/cloudwatch/components/styles.ts +++ b/public/app/plugins/datasource/cloudwatch/components/styles.ts @@ -50,7 +50,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ padding: theme.spacing(1, 1, 1, 0), width: '25%', '&:first-of-type': { - width: '50%', + width: '80%', padding: theme.spacing(1, 1, 1, 2), }, }), diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 67d0eb62b96..835c57385db 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -55,7 +55,7 @@ export class CloudWatchDatasource api: CloudWatchAPI; constructor( - instanceSettings: DataSourceInstanceSettings, + private instanceSettings: DataSourceInstanceSettings, readonly templateSrv: TemplateSrv = getTemplateSrv(), timeSrv: TimeSrv = getTimeSrv() ) { @@ -70,7 +70,6 @@ export class CloudWatchDatasource this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv); this.variables = new CloudWatchVariableSupport(this.api); this.annotations = CloudWatchAnnotationSupport; - this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups; } filterQuery(query: CloudWatchQuery) { @@ -180,7 +179,7 @@ export class CloudWatchDatasource getDefaultQuery(_: CoreApp): Partial { return { - ...getDefaultLogsQuery(this.defaultLogGroups), + ...getDefaultLogsQuery(this.instanceSettings.jsonData.logGroups, this.instanceSettings.jsonData.defaultLogGroups), ...DEFAULT_METRICS_QUERY, }; } diff --git a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts index 42eaf74fa8a..5eff9bbd8cb 100644 --- a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts +++ b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts @@ -1,4 +1,4 @@ -import { CloudWatchLogsQuery, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from './types'; +import { CloudWatchLogsQuery, CloudWatchMetricsQuery, LogGroup, MetricEditorMode, MetricQueryType } from './types'; export const DEFAULT_METRICS_QUERY: Omit = { queryMode: 'Metrics', @@ -16,9 +16,15 @@ export const DEFAULT_METRICS_QUERY: Omit = { matchExact: true, }; -export const getDefaultLogsQuery = (defaultLogGroups?: string[]): Omit => ({ +export const getDefaultLogsQuery = ( + defaultLogGroups?: LogGroup[], + legacyDefaultLogGroups?: string[] +): Omit => ({ id: '', region: 'default', expression: '', - logGroupNames: defaultLogGroups, + // in case legacy default log groups have been defined in the ConfigEditor, they will be migrated in the LogGroupsField component or the next time the ConfigEditor is opened. + // the migration requires async backend calls, so we don't want to do it here as it would block the UI. + logGroupNames: legacyDefaultLogGroups, + logGroups: defaultLogGroups ?? [], }); diff --git a/public/app/plugins/datasource/cloudwatch/hooks.ts b/public/app/plugins/datasource/cloudwatch/hooks.ts index 288c323e5e8..04fefb1b5d4 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.ts @@ -119,20 +119,20 @@ export const useIsMonitoringAccount = (api: CloudWatchAPI, region: string) => { }; export const useAccountOptions = ( - api: Pick, + api: Pick | undefined, region: string ) => { // we call this before the use effect to ensure dependency array below // receives the interpolated value so that the effect is triggered when a variable is changed if (region) { - region = api.templateSrv.replace(region, {}); + region = api?.templateSrv.replace(region, {}) ?? ''; } const fetchAccountOptions = async () => { if (!config.featureToggles.cloudWatchCrossAccountQuerying) { return Promise.resolve([]); } - const accounts = await api.getAccounts({ region }); + const accounts = (await api?.getAccounts({ region })) ?? []; if (accounts.length === 0) { return []; } @@ -143,7 +143,7 @@ export const useAccountOptions = ( description: a.id, })); - const variableOptions = api.getVariables().map(toOption); + const variableOptions = api?.getVariables().map(toOption) || []; const variableOptionGroup: SelectableValue = { label: 'Template Variables', diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index 2516cf85b73..15d129cd036 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -58,7 +58,6 @@ export const LOGSTREAM_IDENTIFIER_INTERNAL = '__logstream__grafana_internal__'; // This class handles execution of CloudWatch logs query data queries export class CloudWatchLogsQueryRunner extends CloudWatchRequest { logsTimeout: string; - defaultLogGroups: string[]; logQueries: Record = {}; tracingDataSourceUid?: string; @@ -71,7 +70,6 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { this.tracingDataSourceUid = instanceSettings.jsonData.tracingDatasourceUid; this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m'; - this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups || []; } /** @@ -89,8 +87,8 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { const startQueryRequests: StartQueryRequest[] = validLogQueries.map((target: CloudWatchLogsQuery) => ({ queryString: target.expression || '', refId: target.refId, - logGroupNames: target.logGroupNames || this.defaultLogGroups, - logGroups: target.logGroups || [], //todo handle defaults + logGroupNames: target.logGroupNames || this.instanceSettings.jsonData.defaultLogGroups || [], + logGroups: target.logGroups || this.instanceSettings.jsonData.logGroups, region: super.replaceVariableAndDisplayWarningIfMulti( this.getActualRegion(target.region), options.scopedVars, diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index b4aa822a928..76f05b0df4e 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -131,6 +131,11 @@ export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData { logsTimeout?: string; // Used to create links if logs contain traceId. tracingDatasourceUid?: string; + + logGroups?: LogGroup[]; + /** + * @deprecated use logGroups + */ defaultLogGroups?: string[]; }