Plugins: Consistent Angular deprecation messages and tracking on docs link click (#71715)

* reportInteraction when clicking on angular deprecation docs link

* Made messages consistent, removed duplicate component

* Revert unnecessary changes in PluginDetailsPage.test.tsx

* Moved angular deprecation notice to different folder

* Fix component names

* Dismissable alert
This commit is contained in:
Giuseppe Guerra 2023-07-24 17:25:36 +02:00 committed by GitHub
parent 20d7cf34b2
commit 7f4d8de6f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 198 additions and 93 deletions

View File

@ -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 (
<div className={styles.wrapper}>
<Alert title="Angular panel plugin" severity="warning">
<div className="markdown-html">
<p>The selected panel plugin is using deprecated plugin APIs.</p>
<ul>
<li>
<a
href="https://grafana.com/docs/grafana/latest/developers/angular_deprecation/"
className="external-link"
target="_blank"
rel="noreferrer"
>
Read more on Angular deprecation
</a>
</li>
<li>
<a href={`plugins/${encodeURIComponent(plugin.meta.id)}`} className="external-link">
View plugin details
</a>
</li>
</ul>
</div>
</Alert>
</div>
);
}
export function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
padding: theme.spacing(1),
}),
};
}

View File

@ -2,12 +2,13 @@ import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { AngularDeprecationPluginNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice';
import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
import { AngularPanelOptions } from './AngularPanelOptions'; import { AngularPanelOptions } from './AngularPanelOptions';
import { AngularPanelPluginWarning } from './AngularPanelPluginWarning';
import { OptionsPaneCategory } from './OptionsPaneCategory'; import { OptionsPaneCategory } from './OptionsPaneCategory';
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
import { getFieldOverrideCategories } from './getFieldOverrideElements'; import { getFieldOverrideCategories } from './getFieldOverrideElements';
@ -101,7 +102,15 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.formBox}> <div className={styles.formBox}>
{panel.isAngularPlugin() && <AngularPanelPluginWarning plugin={plugin} />} {panel.isAngularPlugin() && (
<AngularDeprecationPluginNotice
className={styles.angularDeprecationWrapper}
showPluginDetailsLink={true}
pluginId={plugin.meta.id}
pluginType={plugin.meta.type}
angularSupportEnabled={config?.angularSupportEnabled}
/>
)}
<div className={styles.formRow}> <div className={styles.formRow}>
<FilterInput width={0} value={searchQuery} onChange={setSearchQuery} placeholder={'Search options'} /> <FilterInput width={0} value={searchQuery} onChange={setSearchQuery} placeholder={'Search options'} />
</div> </div>
@ -205,4 +214,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
border-top: none; border-top: none;
flex-grow: 1; flex-grow: 1;
`, `,
angularDeprecationWrapper: css`
padding: ${theme.spacing(1)};
`,
}); });

View File

@ -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 (
<Alert severity="warning" title="Angular plugin" className={className}>
<p>{deprecationMessage(angularSupportEnabled)}</p>
<a
href="https://grafana.com/docs/grafana/latest/developers/angular_deprecation/"
className="external-link"
target="_blank"
rel="noreferrer"
>
Read more about Angular support deprecation.
</a>
</Alert>
);
}

View File

@ -35,7 +35,7 @@ jest.mock('../state/hooks', () => ({
}), }),
})); }));
describe('PluginDetailsAngularDeprecation', () => { describe('PluginDetailsPage Angular deprecation', () => {
afterAll(() => { afterAll(() => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });

View File

@ -9,8 +9,8 @@ import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { AppNotificationSeverity } from 'app/types'; import { AppNotificationSeverity } from 'app/types';
import { AngularDeprecationPluginNotice } from '../../angularDeprecation/AngularDeprecationPluginNotice';
import { Loader } from '../components/Loader'; import { Loader } from '../components/Loader';
import { PluginDetailsAngularDeprecation } from '../components/PluginDetailsAngularDeprecation';
import { PluginDetailsBody } from '../components/PluginDetailsBody'; import { PluginDetailsBody } from '../components/PluginDetailsBody';
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError'; import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
import { PluginDetailsSignature } from '../components/PluginDetailsSignature'; import { PluginDetailsSignature } from '../components/PluginDetailsSignature';
@ -76,9 +76,12 @@ export function PluginDetailsPage({
<Page.Contents> <Page.Contents>
<TabContent className={styles.tabContent}> <TabContent className={styles.tabContent}>
{plugin.angularDetected && ( {plugin.angularDetected && (
<PluginDetailsAngularDeprecation <AngularDeprecationPluginNotice
className={styles.alert} className={styles.alert}
angularSupportEnabled={config?.angularSupportEnabled} angularSupportEnabled={config?.angularSupportEnabled}
pluginId={plugin.id}
pluginType={plugin.type}
showPluginDetailsLink={false}
/> />
)} )}
<PluginDetailsSignature plugin={plugin} className={styles.alert} /> <PluginDetailsSignature plugin={plugin} className={styles.alert} />

View File

@ -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(<AngularDeprecationPluginNotice pluginId="test-id" pluginType={test.pluginType} />);
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(
<AngularDeprecationPluginNotice pluginId="test-id" angularSupportEnabled={test.angularSupportEnabled} />
);
expect(screen.getByText(test.expected)).toBeInTheDocument();
});
});
});
it('displays the plugin details link if showPluginDetailsLink is true', () => {
render(<AngularDeprecationPluginNotice pluginId="test-id" showPluginDetailsLink={true} />);
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(<AngularDeprecationPluginNotice pluginId="test-id" showPluginDetailsLink={false} />);
expect(screen.queryByText(/view plugin details/i)).not.toBeInTheDocument();
});
it('reports interaction when clicking on the link', async () => {
render(<AngularDeprecationPluginNotice pluginId="test-id" />);
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',
});
});
});

View File

@ -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 : (
<Alert severity="warning" title="Angular plugin" className={className} onRemove={() => setDismissed(true)}>
<p>{deprecationMessage(pluginType, angularSupportEnabled)}</p>
<div className="markdown-html">
<ul>
<li>
<a
href="https://grafana.com/docs/grafana/latest/developers/angular_deprecation/"
className="external-link"
target="_blank"
rel="noreferrer"
onClick={() => {
reportInteraction('angular_deprecation_docs_clicked', {
pluginId,
});
}}
>
Read our deprecation notice and migration advice.
</a>
</li>
{showPluginDetailsLink && pluginId ? (
<li>
<a href={`plugins/${encodeURIComponent(pluginId)}`} className="external-link">
View plugin details
</a>
</li>
) : null}
</ul>
</div>
</Alert>
);
}