Datasource Plugins: Allow tracking for configuration usage (#72650)

Datasource Plugins: Allow tracking for configuration usage
This commit is contained in:
Sarah Zinger 2023-08-07 08:31:13 -04:00 committed by GitHub
parent 867162b64a
commit 6ac3348021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 41 deletions

View File

@ -54,7 +54,13 @@ export type DashboardLoadedEventPayload<T> = {
export class DashboardLoadedEvent<T> extends BusEventWithPayload<DashboardLoadedEventPayload<T>> {
static type = 'dashboard-loaded';
}
export class DataSourceUpdatedSuccessfully extends BusEventBase {
static type = 'datasource-updated-successfully';
}
export class DataSourceTestSucceeded extends BusEventBase {
static type = 'datasource-test-succeeded';
}
export class DataSourceTestFailed extends BusEventBase {
static type = 'datasource-test-failed';
}

View File

@ -2,6 +2,7 @@ import { thunkTester } from 'test/core/thunk/thunkTester';
import { AppPluginMeta, DataSourceSettings, PluginMetaInfo, PluginType } from '@grafana/data';
import { FetchError } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { ThunkResult, ThunkDispatch } from 'app/types';
import { getMockDataSource } from '../__mocks__';
@ -30,7 +31,12 @@ import {
jest.mock('../api');
jest.mock('app/core/services/backend_srv');
jest.mock('app/core/core');
jest.mock('app/core/core', () => ({
...jest.requireActual('app/core/core'),
appEvents: {
publish: jest.fn(),
},
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: jest.fn().mockReturnValue({ reload: jest.fn() }),
@ -331,6 +337,39 @@ describe('testDataSource', () => {
const dispatchedActions = await failDataSourceTest(error);
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
});
it('publishes an app event when the test succeeds', async () => {
const dependencies: TestDataSourceDependencies = {
getDatasourceSrv: () => ({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockReturnValue({
status: 'success',
message: '',
}),
type: 'cloudwatch',
uid: 'CW1234',
}),
}),
getBackendSrv: getBackendSrvMock,
};
await thunkTester({})
.givenThunk(testDataSource)
.whenThunkIsDispatched('CloudWatch', DATASOURCES_ROUTES.Edit, dependencies);
expect(appEvents.publish).toHaveBeenCalledWith({ type: 'datasource-test-succeeded' });
});
it('publishes an app event when the test fails', async () => {
const error: FetchError = {
config: {
url: '',
},
data: {},
statusText: 'Bad Request',
status: 400,
};
await failDataSourceTest(error);
expect(appEvents.publish).toHaveBeenCalledWith({ type: 'datasource-test-failed' });
});
});
});

View File

@ -1,15 +1,22 @@
import { DataSourcePluginMeta, DataSourceSettings, locationUtil, TestDataSourceResponse } from '@grafana/data';
import {
DataSourcePluginMeta,
DataSourceSettings,
locationUtil,
TestDataSourceResponse,
DataSourceTestSucceeded,
DataSourceTestFailed,
} from '@grafana/data';
import {
config,
DataSourceSrv,
DataSourceWithBackend,
getDataSourceSrv,
HealthCheckError,
HealthCheckResultDetails,
isFetchError,
locationService,
} from '@grafana/runtime';
import { updateNavIndex } from 'app/core/actions';
import { contextSrv } from 'app/core/core';
import { appEvents, contextSrv } from 'app/core/core';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
@ -53,7 +60,7 @@ export interface InitDataSourceSettingDependencies {
}
export interface TestDataSourceDependencies {
getDatasourceSrv: typeof getDataSourceSrv;
getDatasourceSrv: () => Pick<DataSourceSrv, 'get'>;
getBackendSrv: typeof getBackendSrv;
}
@ -150,6 +157,7 @@ export const testDataSource = (
success: true,
path: editLink,
});
appEvents.publish(new DataSourceTestSucceeded());
} catch (err) {
const formattedError = parseHealthCheckError(err);
@ -161,6 +169,7 @@ export const testDataSource = (
success: false,
path: editLink,
});
appEvents.publish(new DataSourceTestFailed());
}
});
};

View File

@ -4,6 +4,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import { AwsAuthType } from '@grafana/aws-sdk';
import { PluginContextProvider, PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import { CloudWatchSettings, setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
@ -25,12 +26,16 @@ jest.mock('./XrayLinkConfig', () => ({
const putMock = jest.fn();
const getMock = jest.fn();
const mockAppEvents = {
subscribe: () => ({ unsubscribe: jest.fn() }),
};
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
put: putMock,
get: getMock,
}),
getAppEvents: () => mockAppEvents,
}));
const props: Props = {
@ -82,11 +87,21 @@ const setup = (optionOverrides?: Partial<Props['options']>) => {
...optionOverrides,
},
};
const meta: PluginMeta = {
...newProps.options,
id: 'cloudwatch',
type: PluginType.datasource,
info: {} as PluginMetaInfo,
module: '',
baseUrl: '',
};
render(
<Provider store={store}>
<ConfigEditor {...newProps} />
</Provider>
return render(
<PluginContextProvider meta={meta}>
<Provider store={store}>
<ConfigEditor {...newProps} />
</Provider>
</PluginContextProvider>
);
};
@ -177,43 +192,19 @@ describe('Render', () => {
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(<ConfigEditor {...newProps} />);
setup({ version: SAVED_VERSION });
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(<ConfigEditor {...newProps} />);
setup({ version: SAVED_VERSION });
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(<ConfigEditor {...newProps} />);
setup({ version: SAVED_VERSION });
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
await userEvent.click(screen.getByText('Select log groups'));
@ -231,7 +222,20 @@ describe('Render', () => {
version: SAVED_VERSION,
},
};
const { rerender } = render(<ConfigEditor {...newProps} />);
const meta: PluginMeta = {
...newProps.options,
id: 'cloudwatch',
type: PluginType.datasource,
info: {} as PluginMetaInfo,
module: '',
baseUrl: '',
};
const { rerender } = render(
<PluginContextProvider meta={meta}>
<ConfigEditor {...newProps} />
</PluginContextProvider>
);
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
const rerenderProps = {
...newProps,
@ -243,7 +247,11 @@ describe('Render', () => {
},
},
};
rerender(<ConfigEditor {...rerenderProps} />);
rerender(
<PluginContextProvider meta={meta}>
<ConfigEditor {...rerenderProps} />
</PluginContextProvider>
);
await waitFor(() => expect(screen.getByText('AWS SDK Default')).toBeInTheDocument());
await userEvent.click(screen.getByText('Select log groups'));
await waitFor(() =>
@ -264,7 +272,19 @@ describe('Render', () => {
version: SAVED_VERSION,
},
};
const { rerender } = render(<ConfigEditor {...newProps} />);
const meta: PluginMeta = {
...newProps.options,
id: 'cloudwatch',
type: PluginType.datasource,
info: {} as PluginMetaInfo,
module: '',
baseUrl: '',
};
const { rerender } = render(
<PluginContextProvider meta={meta}>
<ConfigEditor {...newProps} />
</PluginContextProvider>
);
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
const rerenderProps = {
...newProps,
@ -273,7 +293,11 @@ describe('Render', () => {
version: 1,
},
};
rerender(<ConfigEditor {...rerenderProps} />);
rerender(
<PluginContextProvider meta={meta}>
<ConfigEditor {...rerenderProps} />
</PluginContextProvider>
);
await userEvent.click(screen.getByText('Select log groups'));
await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument());
});

View File

@ -7,7 +7,10 @@ import {
DataSourcePluginOptionsEditorProps,
onUpdateDatasourceJsonDataOption,
updateDatasourcePluginJsonDataOption,
DataSourceTestSucceeded,
DataSourceTestFailed,
} from '@grafana/data';
import { getAppEvents, usePluginInteractionReporter } from '@grafana/runtime';
import { Input, InlineField, FieldProps, SecureSocksProxySettings } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { config } from 'app/core/config';
@ -37,6 +40,23 @@ export const ConfigEditor = (props: Props) => {
invalid: false,
});
useEffect(() => setLogGroupFieldState({ invalid: false }), [props.options]);
const report = usePluginInteractionReporter();
useEffect(() => {
const successSubscription = getAppEvents().subscribe<DataSourceTestSucceeded>(DataSourceTestSucceeded, () => {
report('grafana_plugin_cloudwatch_save_succeeded', {
auth_type: options.jsonData.authType,
});
});
const failSubscription = getAppEvents().subscribe<DataSourceTestFailed>(DataSourceTestFailed, () => {
report('grafana_plugin_cloudwatch_save_failed', {
auth_type: options.jsonData.authType,
});
});
return () => {
successSubscription.unsubscribe();
failSubscription.unsubscribe();
};
}, [options.jsonData.authType, report]);
return (
<>