diff --git a/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx b/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx index 8308d8ed84c..cb2e0b54ac8 100644 --- a/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx +++ b/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx @@ -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 => ({ testingStatus: { @@ -140,6 +146,7 @@ describe('', () => { 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('', () => { 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) => ( +
{text}
+ )) as ComponentTypeWithExtensionMeta; + 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(); + + 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(); + + expect(screen.queryByText('Blocked Component')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/datasources/components/DataSourceTestingStatus.tsx b/public/app/features/datasources/components/DataSourceTestingStatus.tsx index 317070df7c6..3c8d512067d 100644 --- a/public/app/features/datasources/components/DataSourceTestingStatus.tsx +++ b/public/app/features/datasources/components/DataSourceTestingStatus.tsx @@ -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({ + 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 })} )} + {extensionComponents.length > 0 && ( +
+ {renderLimitedComponents({ + props: extensionStatusContext, + components: extensionComponents, + limit: 2, + pluginId: ALLOWED_DATASOURCE_EXTENSION_PLUGINS, + })} +
+ )} ); diff --git a/public/app/features/datasources/constants.ts b/public/app/features/datasources/constants.ts index 6bd3c7c1a6a..76855556b62 100644 --- a/public/app/features/datasources/constants.ts +++ b/public/app/features/datasources/constants.ts @@ -28,4 +28,5 @@ export const ALLOWED_DATASOURCE_EXTENSION_PLUGINS = [ 'grafana-pyroscope-app', 'grafana-monitoring-app', 'grafana-troubleshooting-app', + 'grafana-assistant-app', ];