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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.", "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"]
],

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 { 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';

View File

@ -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",

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 { 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: {

View File

@ -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';

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 { 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

View File

@ -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()}

View File

@ -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>

View File

@ -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>
</>
);

View File

@ -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();

View File

@ -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} />

View File

@ -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(

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}
}

View File

@ -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 },

View File

@ -4454,6 +4454,7 @@ __metadata:
"@sentry/browser": 6.19.7
"@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