Data source config: Add extension point for components (#110588)

* Data source config: Add extension point for components

* Fix lint

* Fix and add tests

* Refactor allowed plugins

* Fix lint
This commit is contained in:
Ivana Huckova
2025-09-04 14:30:17 +00:00
committed by GitHub
parent f1dffca140
commit f62785d272
3 changed files with 95 additions and 13 deletions
@@ -1,7 +1,12 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { PluginExtensionTypes, PluginExtensionLink } from '@grafana/data';
import { setPluginLinksHook, UsePluginLinksOptions } from '@grafana/runtime';
import {
PluginExtensionTypes,
PluginExtensionLink,
ComponentTypeWithExtensionMeta,
PluginExtensionDataSourceConfigStatusContext,
} from '@grafana/data';
import { setPluginLinksHook, UsePluginLinksOptions, setPluginComponentsHook } from '@grafana/runtime';
import { getMockDataSource } from '../mocks/dataSourcesMocks';
@@ -15,6 +20,7 @@ jest.mock('../../../core/core', () => ({
}));
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
const getProps = (partialProps?: Partial<Props>): Props => ({
testingStatus: {
@@ -140,6 +146,7 @@ describe('<DataSourceTestingStatus />', () => {
afterEach(() => {
// Reset the hook to default empty state
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
});
it('should render plugin links when severity is error and links exist', () => {
@@ -271,4 +278,60 @@ describe('<DataSourceTestingStatus />', () => {
expect(screen.queryByText('Help Documentation')).not.toBeInTheDocument();
});
});
describe('Plugin components', () => {
const createMockComponent = (
overrides: Partial<{
id: string;
title: string;
description: string;
pluginId: string;
text: string;
}> = {}
) => {
const text = overrides.text ?? 'Test Component';
const Comp = ((_props: PluginExtensionDataSourceConfigStatusContext) => (
<div>{text}</div>
)) as ComponentTypeWithExtensionMeta<PluginExtensionDataSourceConfigStatusContext>;
Object.assign(Comp, {
meta: {
id: overrides.id ?? 'test-component',
type: PluginExtensionTypes.component,
title: overrides.title ?? 'Test Component',
description: overrides.description ?? 'Test component description',
pluginId: overrides.pluginId ?? 'grafana-monitoring-app',
},
});
return Comp;
};
afterEach(() => {
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
});
it('should render plugin component from allowed plugin', () => {
const AllowedComponent = createMockComponent({
pluginId: 'grafana-monitoring-app',
text: 'Allowed Component',
}) as unknown as ComponentTypeWithExtensionMeta<{}>;
setPluginComponentsHook(() => ({ components: [AllowedComponent], isLoading: false }));
render(<DataSourceTestingStatus {...getProps()} />);
expect(screen.getByText('Allowed Component')).toBeInTheDocument();
});
it('should NOT render plugin component from non-allowed plugin', () => {
const BlockedComponent = createMockComponent({
pluginId: 'not-allowed-plugin',
text: 'Blocked Component',
}) as unknown as ComponentTypeWithExtensionMeta<{}>;
setPluginComponentsHook(() => ({ components: [BlockedComponent], isLoading: false }));
render(<DataSourceTestingStatus {...getProps()} />);
expect(screen.queryByText('Blocked Component')).not.toBeInTheDocument();
});
});
});
@@ -6,11 +6,12 @@ import {
GrafanaTheme2,
PluginExtensionPoints,
PluginExtensionLink,
PluginExtensionDataSourceConfigStatusContext,
} from '@grafana/data';
import { sanitizeUrl } from '@grafana/data/internal';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { TestingStatus, config, usePluginLinks } from '@grafana/runtime';
import { TestingStatus, config, usePluginLinks, usePluginComponents, renderLimitedComponents } from '@grafana/runtime';
import { AlertVariant, Alert, useTheme2, Link, useStyles2 } from '@grafana/ui';
import { contextSrv } from '../../../core/core';
@@ -161,21 +162,28 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
};
const styles = useStyles2(getTestingStatusStyles);
// Extensions context
const extensionStatusContext: PluginExtensionDataSourceConfigStatusContext = {
dataSource: {
type: dataSource.type,
uid: dataSource.uid,
name: dataSource.name,
typeName: dataSource.typeName,
},
testingStatus,
severity,
};
const { links: allStatusLinks } = usePluginLinks({
extensionPointId: PluginExtensionPoints.DataSourceConfigStatus,
context: {
dataSource: {
type: dataSource.type,
uid: dataSource.uid,
name: dataSource.name,
typeName: dataSource.typeName,
},
testingStatus,
severity,
},
context: extensionStatusContext,
limitPerPlugin: 1,
});
const { components: extensionComponents } = usePluginComponents<PluginExtensionDataSourceConfigStatusContext>({
extensionPointId: PluginExtensionPoints.DataSourceConfigStatus,
});
// Existing error-specific extensions (backward compatibility)
const { links: allErrorLinks } = usePluginLinks({
extensionPointId: PluginExtensionPoints.DataSourceConfigErrorStatus,
@@ -235,6 +243,16 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
})}
</div>
)}
{extensionComponents.length > 0 && (
<div className={styles.linksContainer}>
{renderLimitedComponents<PluginExtensionDataSourceConfigStatusContext>({
props: extensionStatusContext,
components: extensionComponents,
limit: 2,
pluginId: ALLOWED_DATASOURCE_EXTENSION_PLUGINS,
})}
</div>
)}
</Alert>
</div>
);
@@ -28,4 +28,5 @@ export const ALLOWED_DATASOURCE_EXTENSION_PLUGINS = [
'grafana-pyroscope-app',
'grafana-monitoring-app',
'grafana-troubleshooting-app',
'grafana-assistant-app',
];