diff --git a/public/app/features/dashboard/components/PanelEditor/AngularPanelPluginWarning.tsx b/public/app/features/dashboard/components/PanelEditor/AngularPanelPluginWarning.tsx deleted file mode 100644 index 7351b628f19..00000000000 --- a/public/app/features/dashboard/components/PanelEditor/AngularPanelPluginWarning.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, PanelPlugin } from '@grafana/data'; -import { Alert, useStyles2 } from '@grafana/ui'; - -export interface Props { - plugin: PanelPlugin; -} - -export function AngularPanelPluginWarning({ plugin }: Props) { - const styles = useStyles2(getStyles); - - return ( -
- -
-

The selected panel plugin is using deprecated plugin APIs.

- -
-
-
- ); -} - -export function getStyles(theme: GrafanaTheme2) { - return { - wrapper: css({ - padding: theme.spacing(1), - }), - }; -} diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx index 8fec5bc3feb..7db9b36c2a7 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx @@ -2,12 +2,13 @@ import { css } from '@emotion/css'; import React, { useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { AngularDeprecationPluginNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { AngularPanelOptions } from './AngularPanelOptions'; -import { AngularPanelPluginWarning } from './AngularPanelPluginWarning'; import { OptionsPaneCategory } from './OptionsPaneCategory'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; import { getFieldOverrideCategories } from './getFieldOverrideElements'; @@ -101,7 +102,15 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => { return (
- {panel.isAngularPlugin() && } + {panel.isAngularPlugin() && ( + + )}
@@ -205,4 +214,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ border-top: none; flex-grow: 1; `, + angularDeprecationWrapper: css` + padding: ${theme.spacing(1)}; + `, }); diff --git a/public/app/features/plugins/admin/components/PluginDetailsAngularDeprecation.tsx b/public/app/features/plugins/admin/components/PluginDetailsAngularDeprecation.tsx deleted file mode 100644 index c5e9103a6e4..00000000000 --- a/public/app/features/plugins/admin/components/PluginDetailsAngularDeprecation.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -import { Alert } from '@grafana/ui'; - -type Props = { - className?: string; - angularSupportEnabled?: boolean; -}; - -function deprecationMessage(angularSupportEnabled?: boolean): string { - const msg = 'This plugin uses a deprecated, legacy platform based on AngularJS and '; - if (angularSupportEnabled === undefined) { - return msg + ' may be incompatible depending on your Grafana configuration.'; - } - if (angularSupportEnabled) { - return msg + ' will stop working in future releases of Grafana.'; - } - return msg + ' is incompatible with your current Grafana configuration.'; -} - -// An Alert showing information about Angular deprecation notice. -// If the plugin does not use Angular (!plugin.angularDetected), it returns null. -export function PluginDetailsAngularDeprecation({ - className, - angularSupportEnabled, -}: Props): React.ReactElement | null { - return ( - -

{deprecationMessage(angularSupportEnabled)}

- - Read more about Angular support deprecation. - -
- ); -} diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx index 912688b2491..6ff3519d6b2 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx @@ -35,7 +35,7 @@ jest.mock('../state/hooks', () => ({ }), })); -describe('PluginDetailsAngularDeprecation', () => { +describe('PluginDetailsPage Angular deprecation', () => { afterAll(() => { jest.resetAllMocks(); }); diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index 0021f64cac4..afa73b9ac72 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -9,8 +9,8 @@ import { Layout } from '@grafana/ui/src/components/Layout/Layout'; import { Page } from 'app/core/components/Page/Page'; import { AppNotificationSeverity } from 'app/types'; +import { AngularDeprecationPluginNotice } from '../../angularDeprecation/AngularDeprecationPluginNotice'; import { Loader } from '../components/Loader'; -import { PluginDetailsAngularDeprecation } from '../components/PluginDetailsAngularDeprecation'; import { PluginDetailsBody } from '../components/PluginDetailsBody'; import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError'; import { PluginDetailsSignature } from '../components/PluginDetailsSignature'; @@ -76,9 +76,12 @@ export function PluginDetailsPage({ {plugin.angularDetected && ( - )} diff --git a/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.test.tsx b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.test.tsx new file mode 100644 index 00000000000..dde31332d20 --- /dev/null +++ b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.test.tsx @@ -0,0 +1,100 @@ +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { PluginType } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; + +import { AngularDeprecationPluginNotice } from './AngularDeprecationPluginNotice'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +describe('AngularDeprecationPluginNotice', () => { + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('Plugin type message', () => { + const tests = [ + { + name: 'undefined (default)', + pluginType: undefined, + expected: /This plugin uses/i, + }, + { + name: 'app', + pluginType: PluginType.app, + expected: /This app plugin uses/i, + }, + { + name: 'panel', + pluginType: PluginType.panel, + expected: /This panel plugin uses/i, + }, + { + name: 'data source', + pluginType: PluginType.datasource, + expected: /This data source plugin uses/i, + }, + ]; + tests.forEach((test) => { + it(`displays the correct plugin type for ${test.name}`, () => { + render(); + expect(screen.getByText(test.expected)).toBeInTheDocument(); + }); + }); + }); + + describe('Angular configuration', () => { + const tests = [ + { + name: 'undefined (default)', + angularSupportEnabled: undefined, + expected: /may be incompatible/i, + }, + { + name: 'true', + angularSupportEnabled: true, + expected: /will stop working/i, + }, + { + name: 'false', + angularSupportEnabled: false, + expected: /is incompatible/i, + }, + ]; + tests.forEach((test) => { + it(`displays the correct angular configuration for ${test.name}`, () => { + render( + + ); + expect(screen.getByText(test.expected)).toBeInTheDocument(); + }); + }); + }); + + it('displays the plugin details link if showPluginDetailsLink is true', () => { + render(); + const detailsLink = screen.getByText(/view plugin details/i); + expect(detailsLink).toBeInTheDocument(); + expect(detailsLink).toHaveAttribute('href', 'plugins/test-id'); + }); + + it('does not display the plugin details link if showPluginDetailsLink is false', () => { + render(); + expect(screen.queryByText(/view plugin details/i)).not.toBeInTheDocument(); + }); + + it('reports interaction when clicking on the link', async () => { + render(); + const c = 'Read our deprecation notice and migration advice.'; + expect(screen.getByText(c)).toBeInTheDocument(); + await userEvent.click(screen.getByText(c)); + expect(reportInteraction).toHaveBeenCalledWith('angular_deprecation_docs_clicked', { + pluginId: 'test-id', + }); + }); +}); diff --git a/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx new file mode 100644 index 00000000000..485703cf320 --- /dev/null +++ b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; + +import { PluginType } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { Alert } from '@grafana/ui'; + +type Props = { + className?: string; + + pluginId?: string; + pluginType?: PluginType; + + angularSupportEnabled?: boolean; + showPluginDetailsLink?: boolean; +}; + +function deprecationMessage(pluginType?: string, angularSupportEnabled?: boolean): string { + let pluginTypeString: string; + switch (pluginType) { + case PluginType.app: + pluginTypeString = 'app plugin'; + break; + case PluginType.panel: + pluginTypeString = 'panel plugin'; + break; + case PluginType.datasource: + pluginTypeString = 'data source plugin'; + break; + default: + pluginTypeString = 'plugin'; + } + let msg = `This ${pluginTypeString} uses a deprecated, legacy platform based on AngularJS and `; + if (angularSupportEnabled === undefined) { + return msg + ' may be incompatible depending on your Grafana configuration.'; + } + if (angularSupportEnabled) { + return msg + ' will stop working in future releases of Grafana.'; + } + return msg + ' is incompatible with your current Grafana configuration.'; +} + +// An Alert showing information about Angular deprecation notice. +// If the plugin does not use Angular (!plugin.angularDetected), it returns null. +export function AngularDeprecationPluginNotice(props: Props): React.ReactElement | null { + const { className, angularSupportEnabled, pluginId, pluginType, showPluginDetailsLink } = props; + const [dismissed, setDismissed] = useState(false); + return dismissed ? null : ( + setDismissed(true)}> +

{deprecationMessage(pluginType, angularSupportEnabled)}

+ +
+ ); +}