Plugins: Added hook to make it easier to track interactions in plugins (#56126)

* first stab at context away plugin tracking.

* adding a plugin context and a hook to get hold of a tracker that always appends the plugin context information.

* wip

* improved the code a bit.

* wip

* Fixed type errors.

* added datasource_uid to data sources.

* fixed error message when trying to use hook outside of context.

* small refactoring according to feedback.

* using the correct provider for data source context.

* check not needed.

* enforcing the interaction name to start with grafana_plugin_

* exposing guards for the other context type.

* added structure for writing reporter hook tests.

* added some more tests.

* added tests.

* reverted back to inheritance between context types.

* adding mock for getDataSourceSrv
This commit is contained in:
Marcus Andersson
2022-11-02 16:57:57 +01:00
committed by GitHub
parent 932429a545
commit b804b2f073
27 changed files with 591 additions and 104 deletions

View File

@@ -977,6 +977,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "6"], [0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"] [0, 0, 0, "Do not use any type assertions.", "7"]
], ],
"packages/grafana-runtime/src/analytics/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-runtime/src/components/PanelRenderer.tsx:5381": [ "packages/grafana-runtime/src/components/PanelRenderer.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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.", "1"],
@@ -1032,9 +1035,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"] [0, 0, 0, "Unexpected any. Specify a different type.", "2"]
], ],
"packages/grafana-runtime/src/types/analytics.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [ "packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -1050,9 +1050,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Do not use any type assertions.", "12"] [0, 0, 0, "Do not use any type assertions.", "12"]
], ],
"packages/grafana-runtime/src/utils/analytics.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-runtime/src/utils/plugin.ts:5381": [ "packages/grafana-runtime/src/utils/plugin.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],

View File

@@ -0,0 +1,20 @@
import React, { PropsWithChildren, ReactElement, useMemo } from 'react';
import { DataSourceInstanceSettings } from '../../types';
import { Context, DataSourcePluginContextType } from './PluginContext';
export type DataSourcePluginContextProviderProps = {
instanceSettings: DataSourceInstanceSettings;
};
export function DataSourcePluginContextProvider(
props: PropsWithChildren<DataSourcePluginContextProviderProps>
): ReactElement {
const { children, instanceSettings } = props;
const value: DataSourcePluginContextType = useMemo(() => {
return { instanceSettings, meta: instanceSettings.meta };
}, [instanceSettings]);
return <Context.Provider value={value}>{children}</Context.Provider>;
}

View File

@@ -0,0 +1,14 @@
import { createContext } from 'react';
import { DataSourceInstanceSettings } from '../../types/datasource';
import { PluginMeta } from '../../types/plugin';
export interface PluginContextType {
meta: PluginMeta;
}
export interface DataSourcePluginContextType extends PluginContextType {
instanceSettings: DataSourceInstanceSettings;
}
export const Context = createContext<PluginContextType | undefined>(undefined);

View File

@@ -0,0 +1,14 @@
import React, { PropsWithChildren, ReactElement } from 'react';
import { PluginMeta } from '../../types/plugin';
import { Context } from './PluginContext';
export type PluginContextProviderProps = {
meta: PluginMeta;
};
export function PluginContextProvider(props: PropsWithChildren<PluginContextProviderProps>): ReactElement {
const { children, ...rest } = props;
return <Context.Provider value={rest}>{children}</Context.Provider>;
}

View File

@@ -0,0 +1,5 @@
import { type DataSourcePluginContextType, type PluginContextType } from './PluginContext';
export function isDataSourcePluginContext(context: PluginContextType): context is DataSourcePluginContextType {
return 'instanceSettings' in context && 'meta' in context;
}

View File

@@ -0,0 +1,11 @@
import { useContext } from 'react';
import { Context, PluginContextType } from './PluginContext';
export function usePluginContext(): PluginContextType {
const context = useContext(Context);
if (!context) {
throw new Error('usePluginContext must be used within a PluginContextProvider');
}
return context;
}

View File

@@ -26,4 +26,12 @@ export { PanelPlugin, type SetFieldConfigOptionsArgs, type StandardOptionConfig
export { createFieldConfigRegistry } from './panel/registryFactories'; export { createFieldConfigRegistry } from './panel/registryFactories';
export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner'; export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner';
export { type GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix'; export { type GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix';
export { type PluginContextType, type DataSourcePluginContextType } from './context/plugins/PluginContext';
export { type PluginContextProviderProps, PluginContextProvider } from './context/plugins/PluginContextProvider';
export {
type DataSourcePluginContextProviderProps,
DataSourcePluginContextProvider,
} from './context/plugins/DataSourcePluginContextProvider';
export { usePluginContext } from './context/plugins/usePluginContext';
export { isDataSourcePluginContext } from './context/plugins/guards';
export { getLinksSupplier } from './field/fieldOverrides'; export { getLinksSupplier } from './field/fieldOverrides';

View File

@@ -52,6 +52,7 @@
"@rollup/plugin-node-resolve": "13.3.0", "@rollup/plugin-node-resolve": "13.3.0",
"@testing-library/dom": "8.13.0", "@testing-library/dom": "8.13.0",
"@testing-library/react": "12.1.4", "@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3", "@testing-library/user-event": "14.4.3",
"@types/angular": "1.8.4", "@types/angular": "1.8.4",
"@types/history": "4.7.11", "@types/history": "4.7.11",

View File

@@ -0,0 +1,34 @@
import { DataSourceInstanceSettings, PluginMeta } from '@grafana/data';
import { config } from '../../config';
export type PluginEventProperties = {
grafana_version: string;
plugin_type: string;
plugin_version: string;
plugin_id: string;
plugin_name: string;
};
export function createPluginEventProperties(meta: PluginMeta): PluginEventProperties {
return {
grafana_version: config.buildInfo.version,
plugin_type: String(meta.type),
plugin_version: meta.info.version,
plugin_id: meta.id,
plugin_name: meta.name,
};
}
export type DataSourcePluginEventProperties = PluginEventProperties & {
datasource_uid: string;
};
export function createDataSourcePluginEventProperties(
instanceSettings: DataSourceInstanceSettings
): DataSourcePluginEventProperties {
return {
...createPluginEventProperties(instanceSettings.meta),
datasource_uid: instanceSettings.uid,
};
}

View File

@@ -0,0 +1,278 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import {
DataSourceInstanceSettings,
DataSourcePluginContextProvider,
PluginContextProvider,
PluginMeta,
PluginMetaInfo,
PluginSignatureStatus,
PluginType,
} from '@grafana/data';
import { reportInteraction } from '../utils';
import { usePluginInteractionReporter } from './usePluginInteractionReporter';
jest.mock('../utils', () => ({ reportInteraction: jest.fn() }));
const reportInteractionMock = jest.mocked(reportInteraction);
describe('usePluginInteractionReporter', () => {
beforeEach(() => jest.resetAllMocks());
describe('within a panel plugin', () => {
it('should report interaction with plugin context info for internal panel', () => {
const report = renderPluginReporterHook({
id: 'gauge',
name: 'Gauge',
type: PluginType.panel,
info: createPluginMetaInfo({
version: '',
}),
});
report('grafana_plugin_gradient_mode_changed');
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_gradient_mode_changed');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'panel',
plugin_version: '',
plugin_id: 'gauge',
plugin_name: 'Gauge',
});
});
it('should report interaction with plugin context info for external panel', () => {
const report = renderPluginReporterHook({
id: 'grafana-clock-panel',
name: 'Clock',
type: PluginType.panel,
info: createPluginMetaInfo({
version: '2.1.0',
}),
});
report('grafana_plugin_time_zone_changed');
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_time_zone_changed');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'panel',
plugin_version: '2.1.0',
plugin_id: 'grafana-clock-panel',
plugin_name: 'Clock',
});
});
it('should report interaction with plugin context info and extra info provided when reporting', () => {
const report = renderPluginReporterHook({
id: 'grafana-clock-panel',
name: 'Clock',
type: PluginType.panel,
info: createPluginMetaInfo({
version: '2.1.0',
}),
});
report('grafana_plugin_time_zone_changed', {
time_zone: 'Europe/Stockholm',
});
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_time_zone_changed');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'panel',
plugin_version: '2.1.0',
plugin_id: 'grafana-clock-panel',
plugin_name: 'Clock',
time_zone: 'Europe/Stockholm',
});
});
});
describe('within a data source plugin', () => {
it('should report interaction with plugin context info for internal data source', () => {
const report = renderDataSourcePluginReporterHook({
uid: 'qeSI8VV7z',
meta: createPluginMeta({
id: 'prometheus',
name: 'Prometheus',
type: PluginType.datasource,
info: createPluginMetaInfo({
version: '',
}),
}),
});
report('grafana_plugin_query_mode_changed');
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_query_mode_changed');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'datasource',
plugin_version: '',
plugin_id: 'prometheus',
plugin_name: 'Prometheus',
datasource_uid: 'qeSI8VV7z',
});
});
it('should report interaction with plugin context info for external data source', () => {
const report = renderDataSourcePluginReporterHook({
uid: 'PD8C576611E62080A',
meta: createPluginMeta({
id: 'grafana-github-datasource',
name: 'GitHub',
type: PluginType.datasource,
info: createPluginMetaInfo({
version: '1.2.0',
}),
}),
});
report('grafana_plugin_repository_selected');
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_repository_selected');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'datasource',
plugin_version: '1.2.0',
plugin_id: 'grafana-github-datasource',
plugin_name: 'GitHub',
datasource_uid: 'PD8C576611E62080A',
});
});
it('should report interaction with plugin context info and extra info provided when reporting', () => {
const report = renderDataSourcePluginReporterHook({
uid: 'PD8C576611E62080A',
meta: createPluginMeta({
id: 'grafana-github-datasource',
name: 'GitHub',
type: PluginType.datasource,
info: createPluginMetaInfo({
version: '1.2.0',
}),
}),
});
report('grafana_plugin_repository_selected', {
repository: 'grafana/grafana',
});
const [args] = reportInteractionMock.mock.calls;
const [interactionName, properties] = args;
expect(reportInteractionMock.mock.calls.length).toBe(1);
expect(interactionName).toBe('grafana_plugin_repository_selected');
expect(properties).toEqual({
grafana_version: '1.0',
plugin_type: 'datasource',
plugin_version: '1.2.0',
plugin_id: 'grafana-github-datasource',
plugin_name: 'GitHub',
datasource_uid: 'PD8C576611E62080A',
repository: 'grafana/grafana',
});
});
});
describe('ensure interaction name follows convention', () => {
it('should throw name does not start with "grafana_plugin_"', () => {
const report = renderDataSourcePluginReporterHook();
expect(() => report('select_query_type')).toThrow();
});
it('should throw if name is exactly "grafana_plugin_"', () => {
const report = renderPluginReporterHook();
expect(() => report('grafana_plugin_')).toThrow();
});
});
});
function renderPluginReporterHook(meta?: Partial<PluginMeta>): typeof reportInteraction {
const wrapper = ({ children }: React.PropsWithChildren<{}>) => (
<PluginContextProvider meta={createPluginMeta(meta)}>{children}</PluginContextProvider>
);
const { result } = renderHook(() => usePluginInteractionReporter(), { wrapper });
return result.current;
}
function renderDataSourcePluginReporterHook(settings?: Partial<DataSourceInstanceSettings>): typeof reportInteraction {
const wrapper = ({ children }: React.PropsWithChildren<{}>) => (
<DataSourcePluginContextProvider instanceSettings={createDataSourceInstanceSettings(settings)}>
{children}
</DataSourcePluginContextProvider>
);
const { result } = renderHook(() => usePluginInteractionReporter(), { wrapper });
return result.current;
}
function createPluginMeta(meta: Partial<PluginMeta> = {}): PluginMeta {
return {
id: 'gauge',
name: 'Gauge',
type: PluginType.panel,
info: createPluginMetaInfo(),
module: 'app/plugins/panel/gauge/module',
baseUrl: '',
signature: PluginSignatureStatus.internal,
...meta,
};
}
function createPluginMetaInfo(info: Partial<PluginMetaInfo> = {}): PluginMetaInfo {
return {
author: { name: 'Grafana Labs' },
description: 'Standard gauge visualization',
links: [],
logos: {
large: 'public/app/plugins/panel/gauge/img/icon_gauge.svg',
small: 'public/app/plugins/panel/gauge/img/icon_gauge.svg',
},
screenshots: [],
updated: '',
version: '',
...info,
};
}
function createDataSourceInstanceSettings(
settings: Partial<DataSourceInstanceSettings> = {}
): DataSourceInstanceSettings {
const { meta, ...rest } = settings;
return {
id: 1,
uid: '',
name: '',
meta: createPluginMeta(meta),
type: PluginType.datasource,
readOnly: false,
jsonData: {},
access: 'proxy',
...rest,
};
}

View File

@@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { isDataSourcePluginContext, usePluginContext } from '@grafana/data';
import { reportInteraction } from '../utils';
import { createDataSourcePluginEventProperties, createPluginEventProperties } from './eventProperties';
const namePrefix = 'grafana_plugin_';
export function usePluginInteractionReporter(): typeof reportInteraction {
const context = usePluginContext();
return useMemo(() => {
const info = isDataSourcePluginContext(context)
? createDataSourcePluginEventProperties(context.instanceSettings)
: createPluginEventProperties(context.meta);
return (interactionName: string, properties?: Record<string, unknown>) => {
if (!validInteractionName(interactionName)) {
throw new Error(`Interactions reported in plugins should start with: "${namePrefix}".`);
}
return reportInteraction(interactionName, { ...properties, ...info });
};
}, [context]);
}
function validInteractionName(interactionName: string): boolean {
return interactionName.startsWith(namePrefix) && interactionName.length > namePrefix.length;
}

View File

@@ -1,13 +1,14 @@
import { config } from '../config'; import { config } from '../config';
import { locationService } from '../services'; import { locationService } from '../services';
import { getEchoSrv, EchoEventType } from '../services/EchoSrv'; import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
import { import {
ExperimentViewEchoEvent, ExperimentViewEchoEvent,
InteractionEchoEvent, InteractionEchoEvent,
MetaAnalyticsEvent, MetaAnalyticsEvent,
MetaAnalyticsEventPayload, MetaAnalyticsEventPayload,
PageviewEchoEvent, PageviewEchoEvent,
} from '../types/analytics'; } from './types';
/** /**
* Helper function to report meta analytics to the {@link EchoSrv}. * Helper function to report meta analytics to the {@link EchoSrv}.
@@ -42,7 +43,7 @@ export const reportPageview = () => {
* *
* @public * @public
*/ */
export const reportInteraction = (interactionName: string, properties?: Record<string, any>) => { export const reportInteraction = (interactionName: string, properties?: Record<string, unknown>) => {
getEchoSrv().addEvent<InteractionEchoEvent>({ getEchoSrv().addEvent<InteractionEchoEvent>({
type: EchoEventType.Interaction, type: EchoEventType.Interaction,
payload: { payload: {

View File

@@ -5,9 +5,9 @@
*/ */
export * from './services'; export * from './services';
export * from './config'; export * from './config';
export * from './types'; export * from './analytics/types';
export { loadPluginCss, SystemJS, type PluginCssOptions } from './utils/plugin'; export { loadPluginCss, SystemJS, type PluginCssOptions } from './utils/plugin';
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './utils/analytics'; export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils';
export { featureEnabled } from './utils/licensing'; export { featureEnabled } from './utils/licensing';
export { logInfo, logDebug, logWarning, logError } from './utils/logging'; export { logInfo, logDebug, logWarning, logError } from './utils/logging';
export { export {
@@ -35,3 +35,10 @@ export {
type DataSourcePickerProps, type DataSourcePickerProps,
type DataSourcePickerState, type DataSourcePickerState,
} from './components/DataSourcePicker'; } from './components/DataSourcePicker';
export {
type PluginEventProperties,
createPluginEventProperties,
type DataSourcePluginEventProperties,
createDataSourcePluginEventProperties,
} from './analytics/plugins/eventProperties';
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';

View File

@@ -1 +0,0 @@
export * from './analytics';

View File

@@ -3,7 +3,7 @@ import { noop } from 'lodash';
import React, { FC, useCallback, useMemo } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { CoreApp, DataQuery, GrafanaTheme2, LoadingState } from '@grafana/data'; import { CoreApp, DataQuery, DataSourcePluginContextProvider, GrafanaTheme2, LoadingState } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { Alert, Button, useStyles2 } from '@grafana/ui'; import { Alert, Button, useStyles2 } from '@grafana/ui';
import { LokiQuery } from 'app/plugins/datasource/loki/types'; import { LokiQuery } from 'app/plugins/datasource/loki/types';
@@ -49,7 +49,9 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
return null; return null;
} }
if (error || !dataSource || !dataSource?.components?.QueryEditor) { const dsi = getDataSourceSrv().getInstanceSettings(dataSourceName);
if (error || !dataSource || !dataSource?.components?.QueryEditor || !dsi) {
const errorMessage = error?.message || 'Data source plugin does not export any Query Editor component'; const errorMessage = error?.message || 'Data source plugin does not export any Query Editor component';
return <div>Could not load query editor due to: {errorMessage}</div>; return <div>Could not load query editor due to: {errorMessage}</div>;
} }
@@ -65,15 +67,16 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
return ( return (
<> <>
<QueryEditor <DataSourcePluginContextProvider instanceSettings={dsi}>
query={dataQuery} <QueryEditor
queries={[dataQuery]} query={dataQuery}
app={CoreApp.CloudAlerting} queries={[dataQuery]}
onChange={onChangeQuery} app={CoreApp.CloudAlerting}
onRunQuery={noop} onChange={onChangeQuery}
datasource={dataSource} onRunQuery={noop}
/> datasource={dataSource}
/>
</DataSourcePluginContextProvider>
<div className={styles.preview}> <div className={styles.preview}>
<Button type="button" onClick={onRunQueriesClick} disabled={alertPreview?.data.state === LoadingState.Loading}> <Button type="button" onClick={onRunQueriesClick} disabled={alertPreview?.data.state === LoadingState.Loading}>
Preview alerts Preview alerts

View File

@@ -2,7 +2,15 @@ import { css, cx } from '@emotion/css';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { AnnotationEventMappings, AnnotationQuery, DataQuery, DataSourceApi, LoadingState } from '@grafana/data'; import {
AnnotationEventMappings,
AnnotationQuery,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
DataSourcePluginContextProvider,
LoadingState,
} from '@grafana/data';
import { Button, Icon, IconName, Spinner } from '@grafana/ui'; import { Button, Icon, IconName, Spinner } from '@grafana/ui';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -16,6 +24,7 @@ import { AnnotationFieldMapper } from './AnnotationResultMapper';
interface Props { interface Props {
datasource: DataSourceApi; datasource: DataSourceApi;
datasourceInstanceSettings: DataSourceInstanceSettings;
annotation: AnnotationQuery<DataQuery>; annotation: AnnotationQuery<DataQuery>;
onChange: (annotation: AnnotationQuery<DataQuery>) => void; onChange: (annotation: AnnotationQuery<DataQuery>) => void;
} }
@@ -168,7 +177,7 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
}; };
render() { render() {
const { datasource, annotation } = this.props; const { datasource, annotation, datasourceInstanceSettings } = this.props;
const { response } = this.state; const { response } = this.state;
// Find the annotation runner // Find the annotation runner
@@ -180,17 +189,19 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
const query = annotation.target ?? { refId: 'Anno' }; const query = annotation.target ?? { refId: 'Anno' };
return ( return (
<> <>
<QueryEditor <DataSourcePluginContextProvider instanceSettings={datasourceInstanceSettings}>
key={datasource?.name} <QueryEditor
query={query} key={datasource?.name}
datasource={datasource} query={query}
onChange={this.onQueryChange} datasource={datasource}
onRunQuery={this.onRunQuery} onChange={this.onQueryChange}
data={response?.panelData} onRunQuery={this.onRunQuery}
range={getTimeSrv().timeRange()} data={response?.panelData}
annotation={annotation} range={getTimeSrv().timeRange()}
onAnnotationChange={this.onAnnotationChange} annotation={annotation}
/> onAnnotationChange={this.onAnnotationChange}
/>
</DataSourcePluginContextProvider>
{shouldUseMappingUI(datasource) && ( {shouldUseMappingUI(datasource) && (
<> <>
{this.renderStatus()} {this.renderStatus()}

View File

@@ -27,6 +27,8 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
return getDataSourceSrv().get(annotation.datasource); return getDataSourceSrv().get(annotation.datasource);
}, [annotation.datasource]); }, [annotation.datasource]);
const dsi = getDataSourceSrv().getInstanceSettings(annotation.datasource);
const onUpdate = (annotation: AnnotationQuery) => { const onUpdate = (annotation: AnnotationQuery) => {
const list = [...dashboard.annotations.list]; const list = [...dashboard.annotations.list];
list.splice(editIdx, 1, annotation); list.splice(editIdx, 1, annotation);
@@ -115,8 +117,13 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
</HorizontalGroup> </HorizontalGroup>
</Field> </Field>
<h3 className="page-heading">Query</h3> <h3 className="page-heading">Query</h3>
{ds?.annotations && ( {ds?.annotations && dsi && (
<StandardAnnotationQueryEditor datasource={ds} annotation={annotation} onChange={onUpdate} /> <StandardAnnotationQueryEditor
datasource={ds}
datasourceInstanceSettings={dsi}
annotation={annotation}
onChange={onUpdate}
/>
)} )}
{ds && !ds.annotations && <AngularEditorLoader datasource={ds} annotation={annotation} onChange={onUpdate} />} {ds && !ds.annotations && <AngularEditorLoader datasource={ds} annotation={annotation} onChange={onUpdate} />}
</FieldSet> </FieldSet>

View File

@@ -15,6 +15,7 @@ import {
PanelData, PanelData,
PanelPlugin, PanelPlugin,
PanelPluginMeta, PanelPluginMeta,
PluginContextProvider,
TimeRange, TimeRange,
toDataFrameDTO, toDataFrameDTO,
toUtc, toUtc,
@@ -523,26 +524,28 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
return ( return (
<> <>
<div className={panelContentClassNames}> <div className={panelContentClassNames}>
<PanelContextProvider value={this.state.context}> <PluginContextProvider meta={plugin.meta}>
<PanelComponent <PanelContextProvider value={this.state.context}>
id={panel.id} <PanelComponent
data={data} id={panel.id}
title={panel.title} data={data}
timeRange={timeRange} title={panel.title}
timeZone={this.props.dashboard.getTimezone()} timeRange={timeRange}
options={panelOptions} timeZone={this.props.dashboard.getTimezone()}
fieldConfig={panel.fieldConfig} options={panelOptions}
transparent={panel.transparent} fieldConfig={panel.fieldConfig}
width={panelWidth} transparent={panel.transparent}
height={innerPanelHeight} width={panelWidth}
renderCounter={renderCounter} height={innerPanelHeight}
replaceVariables={panel.replaceVariables} renderCounter={renderCounter}
onOptionsChange={this.onOptionsChange} replaceVariables={panel.replaceVariables}
onFieldConfigChange={this.onFieldConfigChange} onOptionsChange={this.onOptionsChange}
onChangeTimeRange={this.onChangeTimeRange} onFieldConfigChange={this.onFieldConfigChange}
eventBus={dashboard.events} onChangeTimeRange={this.onChangeTimeRange}
/> eventBus={dashboard.events}
</PanelContextProvider> />
</PanelContextProvider>
</PluginContextProvider>
</div> </div>
</> </>
); );

View File

@@ -12,6 +12,19 @@ import { missingRightsMessage } from './DataSourceMissingRightsMessage';
import { readOnlyMessage } from './DataSourceReadOnlyMessage'; import { readOnlyMessage } from './DataSourceReadOnlyMessage';
import { EditDataSourceView, ViewProps } from './EditDataSource'; import { EditDataSourceView, ViewProps } from './EditDataSource';
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getDataSourceSrv: jest.fn(() => ({
getInstanceSettings: (uid: string) => ({
uid,
meta: getMockDataSourceMeta(),
}),
})),
};
});
const setup = (props?: Partial<ViewProps>) => { const setup = (props?: Partial<ViewProps>) => {
const store = configureStore(); const store = configureStore();

View File

@@ -1,7 +1,11 @@
import { AnyAction } from '@reduxjs/toolkit'; import { AnyAction } from '@reduxjs/toolkit';
import React from 'react'; import React from 'react';
import { DataSourcePluginMeta, DataSourceSettings as DataSourceSettingsType } from '@grafana/data'; import {
DataSourcePluginContextProvider,
DataSourcePluginMeta,
DataSourceSettings as DataSourceSettingsType,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import PageLoader from 'app/core/components/PageLoader/PageLoader'; import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { DataSourceSettingsState, useDispatch } from 'app/types'; import { DataSourceSettingsState, useDispatch } from 'app/types';
@@ -107,10 +111,10 @@ export function EditDataSourceView({
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights; const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
const hasDataSource = dataSource.id > 0; const hasDataSource = dataSource.id > 0;
const ds = getDataSourceSrv()?.getInstanceSettings(dataSource.uid); const dsi = getDataSourceSrv()?.getInstanceSettings(dataSource.uid);
const hasAlertingEnabled = Boolean(ds?.meta?.alerting ?? false); const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
const isAlertManagerDatasource = ds?.type === 'alertmanager'; const isAlertManagerDatasource = dsi?.type === 'alertmanager';
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource; const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -129,12 +133,16 @@ export function EditDataSourceView({
} }
// TODO - is this needed? // TODO - is this needed?
if (!hasDataSource) { if (!hasDataSource || !dsi) {
return null; return null;
} }
if (pageId) { if (pageId) {
return <DataSourcePluginConfigPage pageId={pageId} plugin={plugin} />; return (
<DataSourcePluginContextProvider instanceSettings={dsi}>
<DataSourcePluginConfigPage pageId={pageId} plugin={plugin} />;
</DataSourcePluginContextProvider>
);
} }
return ( return (
@@ -154,12 +162,14 @@ export function EditDataSourceView({
/> />
{plugin && ( {plugin && (
<DataSourcePluginSettings <DataSourcePluginContextProvider instanceSettings={dsi}>
plugin={plugin} <DataSourcePluginSettings
dataSource={dataSource} plugin={plugin}
dataSourceMeta={dataSourceMeta} dataSource={dataSource}
onModelChange={onOptionsChange} dataSourceMeta={dataSourceMeta}
/> onModelChange={onOptionsChange}
/>
</DataSourcePluginContextProvider>
)} )}
<DataSourceTestingStatus testingStatus={testingStatus} /> <DataSourceTestingStatus testingStatus={testingStatus} />

View File

@@ -21,6 +21,18 @@ jest.mock('app/core/services/context_srv', () => ({
hasPermissionInMetadata: () => true, hasPermissionInMetadata: () => true,
}, },
})); }));
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getDataSourceSrv: jest.fn(() => ({
getInstanceSettings: (uid: string) => ({
uid,
meta: getMockDataSourceMeta(),
}),
})),
};
});
const setup = (uid: string, store: Store) => const setup = (uid: string, store: Store) =>
render( render(

View File

@@ -9,6 +9,7 @@ import {
PanelPlugin, PanelPlugin,
compareArrayValues, compareArrayValues,
compareDataFrameStructures, compareDataFrameStructures,
PluginContextProvider,
} from '@grafana/data'; } from '@grafana/data';
import { PanelRendererProps } from '@grafana/runtime'; import { PanelRendererProps } from '@grafana/runtime';
import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui'; import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui';
@@ -73,24 +74,26 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
return ( return (
<ErrorBoundaryAlert dependencies={[plugin, data]}> <ErrorBoundaryAlert dependencies={[plugin, data]}>
<PanelComponent <PluginContextProvider meta={plugin.meta}>
id={1} <PanelComponent
data={dataWithOverrides} id={1}
title={title} data={dataWithOverrides}
timeRange={dataWithOverrides.timeRange} title={title}
timeZone={timeZone} timeRange={dataWithOverrides.timeRange}
options={optionsWithDefaults!.options} timeZone={timeZone}
fieldConfig={fieldConfig} options={optionsWithDefaults!.options}
transparent={false} fieldConfig={fieldConfig}
width={width} transparent={false}
height={height} width={width}
renderCounter={0} height={height}
replaceVariables={(str: string) => str} renderCounter={0}
onOptionsChange={onOptionsChange} replaceVariables={(str: string) => str}
onFieldConfigChange={onFieldConfigChange} onOptionsChange={onOptionsChange}
onChangeTimeRange={onChangeTimeRange} onFieldConfigChange={onFieldConfigChange}
eventBus={appEvents} onChangeTimeRange={onChangeTimeRange}
/> eventBus={appEvents}
/>
</PluginContextProvider>
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
); );
} }

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React from 'react'; import React from 'react';
import { AppPlugin, GrafanaTheme2, UrlQueryMap } from '@grafana/data'; import { AppPlugin, GrafanaTheme2, PluginContextProvider, UrlQueryMap } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { VersionList } from '../components/VersionList'; import { VersionList } from '../components/VersionList';
@@ -54,7 +54,9 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
if (pageId === configPage.id) { if (pageId === configPage.id) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<configPage.body plugin={pluginConfig} query={queryParams} /> <PluginContextProvider meta={pluginConfig.meta}>
<configPage.body plugin={pluginConfig} query={queryParams} />
</PluginContextProvider>
</div> </div>
); );
} }

View File

@@ -16,6 +16,7 @@ import {
LoadingState, LoadingState,
PanelData, PanelData,
PanelEvents, PanelEvents,
DataSourcePluginContextProvider,
QueryResultMetaNotice, QueryResultMetaNotice,
TimeRange, TimeRange,
toLegacyResponseData, toLegacyResponseData,
@@ -251,19 +252,21 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
if (QueryEditor) { if (QueryEditor) {
return ( return (
<QueryEditor <DataSourcePluginContextProvider instanceSettings={this.props.dataSource}>
key={datasource?.name} <QueryEditor
query={query} key={datasource?.name}
datasource={datasource} query={query}
onChange={onChange} datasource={datasource}
onRunQuery={onRunQuery} onChange={onChange}
onAddQuery={onAddQuery} onRunQuery={onRunQuery}
data={data} onAddQuery={onAddQuery}
range={getTimeSrv().timeRange()} data={data}
queries={queries} range={getTimeSrv().timeRange()}
app={app} queries={queries}
history={history} app={app}
/> history={history}
/>
</DataSourcePluginContextProvider>
); );
} }
} }

View File

@@ -13,7 +13,7 @@ import {
} from './types'; } from './types';
import { filterMetricsQuery } from './utils/utils'; import { filterMetricsQuery } from './utils/utils';
interface CloudWatchOnDashboardLoadedTrackingEvent { type CloudWatchOnDashboardLoadedTrackingEvent = {
grafana_version?: string; grafana_version?: string;
dashboard_id?: string; dashboard_id?: string;
org_id?: number; org_id?: number;
@@ -52,7 +52,7 @@ interface CloudWatchOnDashboardLoadedTrackingEvent {
/* The number of "Insights" queries that are using the code mode. /* The number of "Insights" queries that are using the code mode.
Should be measured in relation to metrics_query_count, e.g metrics_query_builder_count + metrics_query_code_count = metrics_query_count */ Should be measured in relation to metrics_query_count, e.g metrics_query_builder_count + metrics_query_code_count = metrics_query_count */
metrics_query_code_count: number; metrics_query_code_count: number;
} };
export const onDashboardLoadedHandler = ({ export const onDashboardLoadedHandler = ({
payload: { dashboardId, orgId, grafanaVersion, queries }, payload: { dashboardId, orgId, grafanaVersion, queries },

View File

@@ -4454,6 +4454,7 @@ __metadata:
"@sentry/browser": 6.19.7 "@sentry/browser": 6.19.7
"@testing-library/dom": 8.13.0 "@testing-library/dom": 8.13.0
"@testing-library/react": 12.1.4 "@testing-library/react": 12.1.4
"@testing-library/react-hooks": 8.0.1
"@testing-library/user-event": 14.4.3 "@testing-library/user-event": 14.4.3
"@types/angular": 1.8.4 "@types/angular": 1.8.4
"@types/history": 4.7.11 "@types/history": 4.7.11