diff --git a/public/app/features/plugins/admin/components/Badges/PluginAngularBadge.tsx b/public/app/features/plugins/admin/components/Badges/PluginAngularBadge.tsx new file mode 100644 index 00000000000..9202826a9d3 --- /dev/null +++ b/public/app/features/plugins/admin/components/Badges/PluginAngularBadge.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Badge } from '@grafana/ui'; + +export function PluginAngularBadge(): React.ReactElement { + return ( + + ); +} diff --git a/public/app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge.tsx b/public/app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge.tsx index f726ac2fc26..17fd2c452cf 100644 --- a/public/app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge.tsx +++ b/public/app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge.tsx @@ -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,13 +12,7 @@ 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

Update available!

; - } - - return null; + return

Update available!

; } export const getStyles = (theme: GrafanaTheme2) => { diff --git a/public/app/features/plugins/admin/components/Badges/index.ts b/public/app/features/plugins/admin/components/Badges/index.ts index 8a8e0a822e2..a93033c0591 100644 --- a/public/app/features/plugins/admin/components/Badges/index.ts +++ b/public/app/features/plugins/admin/components/Badges/index.ts @@ -2,3 +2,4 @@ export { PluginDisabledBadge } from './PluginDisabledBadge'; export { PluginInstalledBadge } from './PluginInstallBadge'; export { PluginEnterpriseBadge } from './PluginEnterpriseBadge'; export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge'; +export { PluginAngularBadge } from './PluginAngularBadge'; diff --git a/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.test.tsx b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.test.tsx new file mode 100644 index 00000000000..6fd067a9712 --- /dev/null +++ b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.test.tsx @@ -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( + + ); +} + +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); + } + }); + } + ); +}); diff --git a/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx index 651d46b132b..1bead53d3c7 100644 --- a/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx @@ -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 ( - + Install via grafana.com ); diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx new file mode 100644 index 00000000000..74ee6e929e5 --- /dev/null +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx @@ -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( + + + + ); +} + +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(); + } + }); + } + ); +}); diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 1169c4afbc7..86778817a8d 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -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({ ); } - + const shouldDisable = isInstalling || errorInstalling || (!config.angularSupportEnabled && plugin.angularDetected); return ( - ); diff --git a/public/app/features/plugins/admin/components/PluginActions.tsx b/public/app/features/plugins/admin/components/PluginActions.tsx index 055050ef066..ae46d10a774 100644 --- a/public/app/features/plugins/admin/components/PluginActions.tsx +++ b/public/app/features/plugins/admin/components/PluginActions.tsx @@ -42,7 +42,11 @@ export const PluginActions = ({ plugin }: Props) => { {!isInstallControlsDisabled && ( <> {isExternallyManaged ? ( - + ) : ( +

{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 new file mode 100644 index 00000000000..912688b2491 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx @@ -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( + + + + ) + ); + expect(screen.getByText(/angular plugin/i)).toBeVisible(); + }); + + it('does not render the component for non-angular plugins', async () => { + await act(async () => + render( + + + + ) + ); + expect(screen.queryByText(/angular plugin/i)).toBeNull(); + }); +}); diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index ad7b6ab7160..0021f64cac4 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -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({ + {plugin.angularDetected && ( + + )} diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx index e649279fd47..105ca09c50f 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx @@ -75,4 +75,14 @@ describe('PluginListItemBadges', () => { render(); expect(screen.getByText(/update available/i)).toBeVisible(); }); + + it('renders an angular badge (when plugin is angular)', () => { + render(); + expect(screen.getByText(/angular/i)).toBeVisible(); + }); + + it('does not render an angular badge (when plugin is not angular)', () => { + render(); + expect(screen.queryByText(/angular/i)).toBeNull(); + }); }); diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx index bfc21d07130..8cd581f9ae9 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx @@ -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 ( {plugin.isDisabled && } - + {hasUpdate && } + {plugin.angularDetected && } ); } @@ -26,7 +36,8 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { {plugin.isDisabled && } {plugin.isInstalled && } - + {hasUpdate && } + {plugin.angularDetected && } ); } diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 6036a1c4b1a..eda9ad37d8e 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -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, }; } diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 708464557ec..5604170108b 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -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 {