mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
932429a545
commit
b804b2f073
@ -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.", "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": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[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.", "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": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[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, "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": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -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>;
|
||||
}
|
14
packages/grafana-data/src/context/plugins/PluginContext.tsx
Normal file
14
packages/grafana-data/src/context/plugins/PluginContext.tsx
Normal 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);
|
@ -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>;
|
||||
}
|
5
packages/grafana-data/src/context/plugins/guards.ts
Normal file
5
packages/grafana-data/src/context/plugins/guards.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -26,4 +26,12 @@ export { PanelPlugin, type SetFieldConfigOptionsArgs, type StandardOptionConfig
|
||||
export { createFieldConfigRegistry } from './panel/registryFactories';
|
||||
export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner';
|
||||
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';
|
||||
|
@ -52,6 +52,7 @@
|
||||
"@rollup/plugin-node-resolve": "13.3.0",
|
||||
"@testing-library/dom": "8.13.0",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/angular": "1.8.4",
|
||||
"@types/history": "4.7.11",
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { config } from '../config';
|
||||
import { locationService } from '../services';
|
||||
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
|
||||
|
||||
import {
|
||||
ExperimentViewEchoEvent,
|
||||
InteractionEchoEvent,
|
||||
MetaAnalyticsEvent,
|
||||
MetaAnalyticsEventPayload,
|
||||
PageviewEchoEvent,
|
||||
} from '../types/analytics';
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Helper function to report meta analytics to the {@link EchoSrv}.
|
||||
@ -42,7 +43,7 @@ export const reportPageview = () => {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const reportInteraction = (interactionName: string, properties?: Record<string, any>) => {
|
||||
export const reportInteraction = (interactionName: string, properties?: Record<string, unknown>) => {
|
||||
getEchoSrv().addEvent<InteractionEchoEvent>({
|
||||
type: EchoEventType.Interaction,
|
||||
payload: {
|
@ -5,9 +5,9 @@
|
||||
*/
|
||||
export * from './services';
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
export * from './analytics/types';
|
||||
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 { logInfo, logDebug, logWarning, logError } from './utils/logging';
|
||||
export {
|
||||
@ -35,3 +35,10 @@ export {
|
||||
type DataSourcePickerProps,
|
||||
type DataSourcePickerState,
|
||||
} from './components/DataSourcePicker';
|
||||
export {
|
||||
type PluginEventProperties,
|
||||
createPluginEventProperties,
|
||||
type DataSourcePluginEventProperties,
|
||||
createDataSourcePluginEventProperties,
|
||||
} from './analytics/plugins/eventProperties';
|
||||
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './analytics';
|
@ -3,7 +3,7 @@ import { noop } from 'lodash';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
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 { Alert, Button, useStyles2 } from '@grafana/ui';
|
||||
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
||||
@ -49,7 +49,9 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
|
||||
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';
|
||||
return <div>Could not load query editor due to: {errorMessage}</div>;
|
||||
}
|
||||
@ -65,15 +67,16 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryEditor
|
||||
query={dataQuery}
|
||||
queries={[dataQuery]}
|
||||
app={CoreApp.CloudAlerting}
|
||||
onChange={onChangeQuery}
|
||||
onRunQuery={noop}
|
||||
datasource={dataSource}
|
||||
/>
|
||||
|
||||
<DataSourcePluginContextProvider instanceSettings={dsi}>
|
||||
<QueryEditor
|
||||
query={dataQuery}
|
||||
queries={[dataQuery]}
|
||||
app={CoreApp.CloudAlerting}
|
||||
onChange={onChangeQuery}
|
||||
onRunQuery={noop}
|
||||
datasource={dataSource}
|
||||
/>
|
||||
</DataSourcePluginContextProvider>
|
||||
<div className={styles.preview}>
|
||||
<Button type="button" onClick={onRunQueriesClick} disabled={alertPreview?.data.state === LoadingState.Loading}>
|
||||
Preview alerts
|
||||
|
@ -2,7 +2,15 @@ import { css, cx } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
@ -16,6 +24,7 @@ import { AnnotationFieldMapper } from './AnnotationResultMapper';
|
||||
|
||||
interface Props {
|
||||
datasource: DataSourceApi;
|
||||
datasourceInstanceSettings: DataSourceInstanceSettings;
|
||||
annotation: AnnotationQuery<DataQuery>;
|
||||
onChange: (annotation: AnnotationQuery<DataQuery>) => void;
|
||||
}
|
||||
@ -168,7 +177,7 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, annotation } = this.props;
|
||||
const { datasource, annotation, datasourceInstanceSettings } = this.props;
|
||||
const { response } = this.state;
|
||||
|
||||
// Find the annotation runner
|
||||
@ -180,17 +189,19 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
const query = annotation.target ?? { refId: 'Anno' };
|
||||
return (
|
||||
<>
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={this.onQueryChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
data={response?.panelData}
|
||||
range={getTimeSrv().timeRange()}
|
||||
annotation={annotation}
|
||||
onAnnotationChange={this.onAnnotationChange}
|
||||
/>
|
||||
<DataSourcePluginContextProvider instanceSettings={datasourceInstanceSettings}>
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={this.onQueryChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
data={response?.panelData}
|
||||
range={getTimeSrv().timeRange()}
|
||||
annotation={annotation}
|
||||
onAnnotationChange={this.onAnnotationChange}
|
||||
/>
|
||||
</DataSourcePluginContextProvider>
|
||||
{shouldUseMappingUI(datasource) && (
|
||||
<>
|
||||
{this.renderStatus()}
|
||||
|
@ -27,6 +27,8 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
return getDataSourceSrv().get(annotation.datasource);
|
||||
}, [annotation.datasource]);
|
||||
|
||||
const dsi = getDataSourceSrv().getInstanceSettings(annotation.datasource);
|
||||
|
||||
const onUpdate = (annotation: AnnotationQuery) => {
|
||||
const list = [...dashboard.annotations.list];
|
||||
list.splice(editIdx, 1, annotation);
|
||||
@ -115,8 +117,13 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
|
||||
</HorizontalGroup>
|
||||
</Field>
|
||||
<h3 className="page-heading">Query</h3>
|
||||
{ds?.annotations && (
|
||||
<StandardAnnotationQueryEditor datasource={ds} annotation={annotation} onChange={onUpdate} />
|
||||
{ds?.annotations && dsi && (
|
||||
<StandardAnnotationQueryEditor
|
||||
datasource={ds}
|
||||
datasourceInstanceSettings={dsi}
|
||||
annotation={annotation}
|
||||
onChange={onUpdate}
|
||||
/>
|
||||
)}
|
||||
{ds && !ds.annotations && <AngularEditorLoader datasource={ds} annotation={annotation} onChange={onUpdate} />}
|
||||
</FieldSet>
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
PanelData,
|
||||
PanelPlugin,
|
||||
PanelPluginMeta,
|
||||
PluginContextProvider,
|
||||
TimeRange,
|
||||
toDataFrameDTO,
|
||||
toUtc,
|
||||
@ -523,26 +524,28 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<div className={panelContentClassNames}>
|
||||
<PanelContextProvider value={this.state.context}>
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
data={data}
|
||||
title={panel.title}
|
||||
timeRange={timeRange}
|
||||
timeZone={this.props.dashboard.getTimezone()}
|
||||
options={panelOptions}
|
||||
fieldConfig={panel.fieldConfig}
|
||||
transparent={panel.transparent}
|
||||
width={panelWidth}
|
||||
height={innerPanelHeight}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={panel.replaceVariables}
|
||||
onOptionsChange={this.onOptionsChange}
|
||||
onFieldConfigChange={this.onFieldConfigChange}
|
||||
onChangeTimeRange={this.onChangeTimeRange}
|
||||
eventBus={dashboard.events}
|
||||
/>
|
||||
</PanelContextProvider>
|
||||
<PluginContextProvider meta={plugin.meta}>
|
||||
<PanelContextProvider value={this.state.context}>
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
data={data}
|
||||
title={panel.title}
|
||||
timeRange={timeRange}
|
||||
timeZone={this.props.dashboard.getTimezone()}
|
||||
options={panelOptions}
|
||||
fieldConfig={panel.fieldConfig}
|
||||
transparent={panel.transparent}
|
||||
width={panelWidth}
|
||||
height={innerPanelHeight}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={panel.replaceVariables}
|
||||
onOptionsChange={this.onOptionsChange}
|
||||
onFieldConfigChange={this.onFieldConfigChange}
|
||||
onChangeTimeRange={this.onChangeTimeRange}
|
||||
eventBus={dashboard.events}
|
||||
/>
|
||||
</PanelContextProvider>
|
||||
</PluginContextProvider>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -12,6 +12,19 @@ import { missingRightsMessage } from './DataSourceMissingRightsMessage';
|
||||
import { readOnlyMessage } from './DataSourceReadOnlyMessage';
|
||||
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 store = configureStore();
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
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 PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { DataSourceSettingsState, useDispatch } from 'app/types';
|
||||
@ -107,10 +111,10 @@ export function EditDataSourceView({
|
||||
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
|
||||
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 isAlertManagerDatasource = ds?.type === 'alertmanager';
|
||||
const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
|
||||
const isAlertManagerDatasource = dsi?.type === 'alertmanager';
|
||||
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
|
||||
|
||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@ -129,12 +133,16 @@ export function EditDataSourceView({
|
||||
}
|
||||
|
||||
// TODO - is this needed?
|
||||
if (!hasDataSource) {
|
||||
if (!hasDataSource || !dsi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pageId) {
|
||||
return <DataSourcePluginConfigPage pageId={pageId} plugin={plugin} />;
|
||||
return (
|
||||
<DataSourcePluginContextProvider instanceSettings={dsi}>
|
||||
<DataSourcePluginConfigPage pageId={pageId} plugin={plugin} />;
|
||||
</DataSourcePluginContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -154,12 +162,14 @@ export function EditDataSourceView({
|
||||
/>
|
||||
|
||||
{plugin && (
|
||||
<DataSourcePluginSettings
|
||||
plugin={plugin}
|
||||
dataSource={dataSource}
|
||||
dataSourceMeta={dataSourceMeta}
|
||||
onModelChange={onOptionsChange}
|
||||
/>
|
||||
<DataSourcePluginContextProvider instanceSettings={dsi}>
|
||||
<DataSourcePluginSettings
|
||||
plugin={plugin}
|
||||
dataSource={dataSource}
|
||||
dataSourceMeta={dataSourceMeta}
|
||||
onModelChange={onOptionsChange}
|
||||
/>
|
||||
</DataSourcePluginContextProvider>
|
||||
)}
|
||||
|
||||
<DataSourceTestingStatus testingStatus={testingStatus} />
|
||||
|
@ -21,6 +21,18 @@ jest.mock('app/core/services/context_srv', () => ({
|
||||
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) =>
|
||||
render(
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
PanelPlugin,
|
||||
compareArrayValues,
|
||||
compareDataFrameStructures,
|
||||
PluginContextProvider,
|
||||
} from '@grafana/data';
|
||||
import { PanelRendererProps } from '@grafana/runtime';
|
||||
import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui';
|
||||
@ -73,24 +74,26 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
|
||||
|
||||
return (
|
||||
<ErrorBoundaryAlert dependencies={[plugin, data]}>
|
||||
<PanelComponent
|
||||
id={1}
|
||||
data={dataWithOverrides}
|
||||
title={title}
|
||||
timeRange={dataWithOverrides.timeRange}
|
||||
timeZone={timeZone}
|
||||
options={optionsWithDefaults!.options}
|
||||
fieldConfig={fieldConfig}
|
||||
transparent={false}
|
||||
width={width}
|
||||
height={height}
|
||||
renderCounter={0}
|
||||
replaceVariables={(str: string) => str}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onFieldConfigChange={onFieldConfigChange}
|
||||
onChangeTimeRange={onChangeTimeRange}
|
||||
eventBus={appEvents}
|
||||
/>
|
||||
<PluginContextProvider meta={plugin.meta}>
|
||||
<PanelComponent
|
||||
id={1}
|
||||
data={dataWithOverrides}
|
||||
title={title}
|
||||
timeRange={dataWithOverrides.timeRange}
|
||||
timeZone={timeZone}
|
||||
options={optionsWithDefaults!.options}
|
||||
fieldConfig={fieldConfig}
|
||||
transparent={false}
|
||||
width={width}
|
||||
height={height}
|
||||
renderCounter={0}
|
||||
replaceVariables={(str: string) => str}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onFieldConfigChange={onFieldConfigChange}
|
||||
onChangeTimeRange={onChangeTimeRange}
|
||||
eventBus={appEvents}
|
||||
/>
|
||||
</PluginContextProvider>
|
||||
</ErrorBoundaryAlert>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
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 { VersionList } from '../components/VersionList';
|
||||
@ -54,7 +54,9 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
||||
if (pageId === configPage.id) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<configPage.body plugin={pluginConfig} query={queryParams} />
|
||||
<PluginContextProvider meta={pluginConfig.meta}>
|
||||
<configPage.body plugin={pluginConfig} query={queryParams} />
|
||||
</PluginContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelEvents,
|
||||
DataSourcePluginContextProvider,
|
||||
QueryResultMetaNotice,
|
||||
TimeRange,
|
||||
toLegacyResponseData,
|
||||
@ -251,19 +252,21 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
|
||||
if (QueryEditor) {
|
||||
return (
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
onAddQuery={onAddQuery}
|
||||
data={data}
|
||||
range={getTimeSrv().timeRange()}
|
||||
queries={queries}
|
||||
app={app}
|
||||
history={history}
|
||||
/>
|
||||
<DataSourcePluginContextProvider instanceSettings={this.props.dataSource}>
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
onAddQuery={onAddQuery}
|
||||
data={data}
|
||||
range={getTimeSrv().timeRange()}
|
||||
queries={queries}
|
||||
app={app}
|
||||
history={history}
|
||||
/>
|
||||
</DataSourcePluginContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from './types';
|
||||
import { filterMetricsQuery } from './utils/utils';
|
||||
|
||||
interface CloudWatchOnDashboardLoadedTrackingEvent {
|
||||
type CloudWatchOnDashboardLoadedTrackingEvent = {
|
||||
grafana_version?: string;
|
||||
dashboard_id?: string;
|
||||
org_id?: number;
|
||||
@ -52,7 +52,7 @@ interface CloudWatchOnDashboardLoadedTrackingEvent {
|
||||
/* 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 */
|
||||
metrics_query_code_count: number;
|
||||
}
|
||||
};
|
||||
|
||||
export const onDashboardLoadedHandler = ({
|
||||
payload: { dashboardId, orgId, grafanaVersion, queries },
|
||||
|
Loading…
Reference in New Issue
Block a user