mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 20:29:30 +08:00
Catalog: Display badges for Angular plugins and disable install if Angular is disabled (#69084)
* Angular deprecation: Add Angular badge in plugin catalog page * Angular deprecation: Add alert in plugin details page * Angular deprecation: Disable install button if for Angular plugins * removed extra console.log * Add tests for Angular badge * Add tests for PluginDetailsAngularDeprecation * Add tests for InstallControlsButton * Add tests for ExternallyManagedButton * Table tests * Catalog: Update angular deprecation message * PR review feedback * Update tests * Update copy for angular tooltip and alert * Update tests * Fix test warnings * Fix angularDetected not being set for remote catalog plugins * Dynamic alert text based on grafana config * Moved deprecation message to a separate function * Removed unused Props in PluginAngularBadge
This commit is contained in:
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Badge } from '@grafana/ui';
|
||||
|
||||
export function PluginAngularBadge(): React.ReactElement {
|
||||
return (
|
||||
<Badge
|
||||
icon="exclamation-triangle"
|
||||
text="Angular"
|
||||
color="orange"
|
||||
tooltip="This plugin uses deprecated functionality, support for which is being removed."
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CatalogPlugin } from '../../types';
|
||||
@ -12,15 +12,9 @@ type Props = {
|
||||
|
||||
export function PluginUpdateAvailableBadge({ plugin }: Props): React.ReactElement | null {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// Currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall.
|
||||
if (plugin.hasUpdate && !plugin.isCore && plugin.type !== PluginType.renderer) {
|
||||
return <p className={styles.hasUpdate}>Update available!</p>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
hasUpdate: css`
|
||||
|
@ -2,3 +2,4 @@ export { PluginDisabledBadge } from './PluginDisabledBadge';
|
||||
export { PluginInstalledBadge } from './PluginInstallBadge';
|
||||
export { PluginEnterpriseBadge } from './PluginEnterpriseBadge';
|
||||
export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge';
|
||||
export { PluginAngularBadge } from './PluginAngularBadge';
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { PluginStatus } from '../../types';
|
||||
|
||||
import { ExternallyManagedButton } from './ExternallyManagedButton';
|
||||
|
||||
function setup(opts: { angularSupportEnabled: boolean; angularDetected: boolean }) {
|
||||
config.angularSupportEnabled = opts.angularSupportEnabled;
|
||||
render(
|
||||
<ExternallyManagedButton
|
||||
pluginId={'some-plugin-id'}
|
||||
angularDetected={opts.angularDetected}
|
||||
pluginStatus={PluginStatus.INSTALL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ExternallyManagedButton', () => {
|
||||
let oldAngularSupportEnabled = config.angularSupportEnabled;
|
||||
afterAll(() => {
|
||||
config.angularSupportEnabled = oldAngularSupportEnabled;
|
||||
});
|
||||
|
||||
describe.each([{ angularSupportEnabled: true }, { angularSupportEnabled: false }])(
|
||||
'angular support is $angularSupportEnabled',
|
||||
({ angularSupportEnabled }) => {
|
||||
it.each([
|
||||
{ angularDetected: true, expectEnabled: angularSupportEnabled },
|
||||
{ angularDetected: false, expectEnabled: true },
|
||||
])('angular detected is $angularDetected', ({ angularDetected, expectEnabled }) => {
|
||||
setup({ angularSupportEnabled, angularDetected });
|
||||
|
||||
const el = screen.getByRole('link');
|
||||
expect(el).toHaveTextContent(/install/i);
|
||||
expect(el).toBeVisible();
|
||||
const linkDisabledStyle = 'pointer-events: none';
|
||||
if (expectEnabled) {
|
||||
expect(el).not.toHaveStyle(linkDisabledStyle);
|
||||
} else {
|
||||
expect(el).toHaveStyle(linkDisabledStyle);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { getExternalManageLink } from '../../helpers';
|
||||
@ -8,9 +9,10 @@ import { PluginStatus } from '../../types';
|
||||
type ExternallyManagedButtonProps = {
|
||||
pluginId: string;
|
||||
pluginStatus: PluginStatus;
|
||||
angularDetected?: boolean;
|
||||
};
|
||||
|
||||
export function ExternallyManagedButton({ pluginId, pluginStatus }: ExternallyManagedButtonProps) {
|
||||
export function ExternallyManagedButton({ pluginId, pluginStatus, angularDetected }: ExternallyManagedButtonProps) {
|
||||
const externalManageLink = `${getExternalManageLink(pluginId)}/?tab=installation`;
|
||||
|
||||
if (pluginStatus === PluginStatus.UPDATE) {
|
||||
@ -35,7 +37,12 @@ export function ExternallyManagedButton({ pluginId, pluginStatus }: ExternallyMa
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||
<LinkButton
|
||||
disabled={!config.angularSupportEnabled && angularDetected}
|
||||
href={externalManageLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Install via grafana.com
|
||||
</LinkButton>
|
||||
);
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { CatalogPlugin, PluginStatus } from '../../types';
|
||||
|
||||
import { InstallControlsButton } from './InstallControlsButton';
|
||||
|
||||
const plugin: CatalogPlugin = {
|
||||
description: 'The test plugin',
|
||||
downloads: 5,
|
||||
id: 'test-plugin',
|
||||
info: {
|
||||
logos: { small: '', large: '' },
|
||||
},
|
||||
name: 'Testing Plugin',
|
||||
orgName: 'Test',
|
||||
popularity: 0,
|
||||
signature: PluginSignatureStatus.valid,
|
||||
publishedAt: '2020-09-01',
|
||||
updatedAt: '2021-06-28',
|
||||
hasUpdate: false,
|
||||
isInstalled: false,
|
||||
isCore: false,
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
isDisabled: false,
|
||||
isPublished: true,
|
||||
};
|
||||
|
||||
function setup(opts: { angularSupportEnabled: boolean; angularDetected: boolean }) {
|
||||
config.angularSupportEnabled = opts.angularSupportEnabled;
|
||||
render(
|
||||
<TestProvider>
|
||||
<InstallControlsButton
|
||||
plugin={{ ...plugin, angularDetected: opts.angularDetected }}
|
||||
pluginStatus={PluginStatus.INSTALL}
|
||||
/>
|
||||
</TestProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('InstallControlsButton', () => {
|
||||
let oldAngularSupportEnabled = config.angularSupportEnabled;
|
||||
afterAll(() => {
|
||||
config.angularSupportEnabled = oldAngularSupportEnabled;
|
||||
});
|
||||
|
||||
describe.each([{ angularSupportEnabled: true }, { angularSupportEnabled: false }])(
|
||||
'angular support is $angularSupportEnabled',
|
||||
({ angularSupportEnabled }) => {
|
||||
it.each([
|
||||
{ angularDetected: true, expectEnabled: angularSupportEnabled },
|
||||
{ angularDetected: false, expectEnabled: true },
|
||||
])('angular detected is $angularDetected', ({ angularDetected, expectEnabled }) => {
|
||||
setup({ angularSupportEnabled, angularDetected });
|
||||
|
||||
const el = screen.getByRole('button');
|
||||
expect(el).toHaveTextContent(/install/i);
|
||||
expect(el).toBeVisible();
|
||||
if (expectEnabled) {
|
||||
expect(el).toBeEnabled();
|
||||
} else {
|
||||
expect(el).toBeDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, ConfirmModal } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
@ -17,7 +17,7 @@ type InstallControlsButtonProps = {
|
||||
plugin: CatalogPlugin;
|
||||
pluginStatus: PluginStatus;
|
||||
latestCompatibleVersion?: Version;
|
||||
setNeedReload: (needReload: boolean) => void;
|
||||
setNeedReload?: (needReload: boolean) => void;
|
||||
};
|
||||
|
||||
export function InstallControlsButton({
|
||||
@ -58,7 +58,7 @@ export function InstallControlsButton({
|
||||
if (!errorInstalling && !('error' in result)) {
|
||||
appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]);
|
||||
if (plugin.type === 'app') {
|
||||
setNeedReload(true);
|
||||
setNeedReload?.(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -77,7 +77,7 @@ export function InstallControlsButton({
|
||||
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]);
|
||||
if (plugin.type === 'app') {
|
||||
dispatch(removePluginFromNavTree({ pluginID: plugin.id }));
|
||||
setNeedReload(false);
|
||||
setNeedReload?.(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -122,9 +122,9 @@ export function InstallControlsButton({
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const shouldDisable = isInstalling || errorInstalling || (!config.angularSupportEnabled && plugin.angularDetected);
|
||||
return (
|
||||
<Button disabled={isInstalling || errorInstalling} onClick={onInstall}>
|
||||
<Button disabled={shouldDisable} onClick={onInstall}>
|
||||
{isInstalling ? 'Installing' : 'Install'}
|
||||
</Button>
|
||||
);
|
||||
|
@ -42,7 +42,11 @@ export const PluginActions = ({ plugin }: Props) => {
|
||||
{!isInstallControlsDisabled && (
|
||||
<>
|
||||
{isExternallyManaged ? (
|
||||
<ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />
|
||||
<ExternallyManagedButton
|
||||
pluginId={plugin.id}
|
||||
pluginStatus={pluginStatus}
|
||||
angularDetected={plugin.angularDetected}
|
||||
/>
|
||||
) : (
|
||||
<InstallControlsButton
|
||||
plugin={plugin}
|
||||
|
@ -0,0 +1,40 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
|
||||
import { PluginDetailsPage } from './PluginDetailsPage';
|
||||
|
||||
jest.mock('../state/hooks', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('../state/hooks'),
|
||||
useGetSingle: jest.fn().mockImplementation((id: string) => {
|
||||
return {
|
||||
description: 'The test plugin',
|
||||
downloads: 5,
|
||||
id: 'test-plugin',
|
||||
info: {
|
||||
logos: { small: '', large: '' },
|
||||
},
|
||||
name: 'Testing Plugin',
|
||||
orgName: 'Test',
|
||||
popularity: 0,
|
||||
signature: PluginSignatureStatus.valid,
|
||||
publishedAt: '2020-09-01',
|
||||
updatedAt: '2021-06-28',
|
||||
hasUpdate: false,
|
||||
isInstalled: false,
|
||||
isCore: false,
|
||||
isDev: false,
|
||||
isEnterprise: false,
|
||||
isDisabled: false,
|
||||
isPublished: true,
|
||||
angularDetected: id === 'angular',
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PluginDetailsAngularDeprecation', () => {
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders the component for angular plugins', async () => {
|
||||
await act(async () =>
|
||||
render(
|
||||
<TestProvider>
|
||||
<PluginDetailsPage pluginId="angular" />
|
||||
</TestProvider>
|
||||
)
|
||||
);
|
||||
expect(screen.getByText(/angular plugin/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not render the component for non-angular plugins', async () => {
|
||||
await act(async () =>
|
||||
render(
|
||||
<TestProvider>
|
||||
<PluginDetailsPage pluginId="not-angular" />
|
||||
</TestProvider>
|
||||
)
|
||||
);
|
||||
expect(screen.queryByText(/angular plugin/i)).toBeNull();
|
||||
});
|
||||
});
|
@ -3,12 +3,14 @@ import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2, TabContent, Alert } from '@grafana/ui';
|
||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
|
||||
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';
|
||||
@ -73,6 +75,12 @@ export function PluginDetailsPage({
|
||||
<Page navId={navId} pageNav={navModel} actions={actions} subTitle={subtitle} info={info}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
{plugin.angularDetected && (
|
||||
<PluginDetailsAngularDeprecation
|
||||
className={styles.alert}
|
||||
angularSupportEnabled={config?.angularSupportEnabled}
|
||||
/>
|
||||
)}
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} />
|
||||
|
@ -75,4 +75,14 @@ describe('PluginListItemBadges', () => {
|
||||
render(<PluginListItemBadges plugin={{ ...plugin, hasUpdate: true, installedVersion: '0.0.9' }} />);
|
||||
expect(screen.getByText(/update available/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an angular badge (when plugin is angular)', () => {
|
||||
render(<PluginListItemBadges plugin={{ ...plugin, angularDetected: true }} />);
|
||||
expect(screen.getByText(/angular/i)).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not render an angular badge (when plugin is not angular)', () => {
|
||||
render(<PluginListItemBadges plugin={{ ...plugin, angularDetected: false }} />);
|
||||
expect(screen.queryByText(/angular/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
import { HorizontalGroup, PluginSignatureBadge } from '@grafana/ui';
|
||||
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
import { PluginEnterpriseBadge, PluginDisabledBadge, PluginInstalledBadge, PluginUpdateAvailableBadge } from './Badges';
|
||||
import {
|
||||
PluginEnterpriseBadge,
|
||||
PluginDisabledBadge,
|
||||
PluginInstalledBadge,
|
||||
PluginUpdateAvailableBadge,
|
||||
PluginAngularBadge,
|
||||
} from './Badges';
|
||||
|
||||
type PluginBadgeType = {
|
||||
plugin: CatalogPlugin;
|
||||
};
|
||||
|
||||
export function PluginListItemBadges({ plugin }: PluginBadgeType) {
|
||||
// Currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall.
|
||||
const hasUpdate = plugin.hasUpdate && !plugin.isCore && plugin.type !== PluginType.renderer;
|
||||
if (plugin.isEnterprise) {
|
||||
return (
|
||||
<HorizontalGroup height="auto" wrap>
|
||||
<PluginEnterpriseBadge plugin={plugin} />
|
||||
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
|
||||
<PluginUpdateAvailableBadge plugin={plugin} />
|
||||
{hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />}
|
||||
{plugin.angularDetected && <PluginAngularBadge />}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
@ -26,7 +36,8 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
|
||||
<PluginSignatureBadge status={plugin.signature} />
|
||||
{plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
|
||||
{plugin.isInstalled && <PluginInstalledBadge />}
|
||||
<PluginUpdateAvailableBadge plugin={plugin} />
|
||||
{hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />}
|
||||
{plugin.angularDetected && <PluginAngularBadge />}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
|
||||
updatedAt,
|
||||
createdAt: publishedAt,
|
||||
status,
|
||||
angularDetected,
|
||||
} = plugin;
|
||||
|
||||
const isDisabled = !!error || isDisabledSecretsPlugin(typeCode);
|
||||
@ -90,6 +91,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
|
||||
isEnterprise: status === 'enterprise',
|
||||
type: typeCode,
|
||||
error: error?.errorCode,
|
||||
angularDetected,
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,6 +107,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
|
||||
signatureType,
|
||||
hasUpdate,
|
||||
accessControl,
|
||||
angularDetected,
|
||||
} = plugin;
|
||||
|
||||
const isDisabled = !!error || isDisabledSecretsPlugin(type);
|
||||
@ -132,6 +135,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
|
||||
type,
|
||||
error: error?.errorCode,
|
||||
accessControl: accessControl,
|
||||
angularDetected,
|
||||
};
|
||||
}
|
||||
|
||||
@ -186,6 +190,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
|
||||
error: error?.errorCode,
|
||||
// Only local plugins have access control metadata
|
||||
accessControl: local?.accessControl,
|
||||
angularDetected: local?.angularDetected || remote?.angularDetected,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
|
||||
installedVersion?: string;
|
||||
details?: CatalogPluginDetails;
|
||||
error?: PluginErrorCode;
|
||||
angularDetected?: boolean;
|
||||
}
|
||||
|
||||
export interface CatalogPluginDetails {
|
||||
@ -123,6 +124,7 @@ export type RemotePlugin = {
|
||||
versionSignedByOrg: string;
|
||||
versionSignedByOrgName: string;
|
||||
versionStatus: string;
|
||||
angularDetected?: boolean;
|
||||
};
|
||||
|
||||
export type LocalPlugin = WithAccessControlMetadata & {
|
||||
@ -156,6 +158,7 @@ export type LocalPlugin = WithAccessControlMetadata & {
|
||||
state: string;
|
||||
type: PluginType;
|
||||
dependencies: PluginDependencies;
|
||||
angularDetected: boolean;
|
||||
};
|
||||
|
||||
interface Rel {
|
||||
|
Reference in New Issue
Block a user