mirror of https://github.com/grafana/grafana.git
				
				
				
			Plugins: Show deprecated plugins (#74598)
* feat: add a `isDeprecated` field to `CatalogPlugin` * tests: update the tests for merging local & remote * feat: display a deprecated badge in the plugins list * feat: show a deprecated warning if the plugin is deprecated * Fix linting issues * Review notes Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * refactor: remove `isDeprecated` from the details (it's already in the main CatalogPlugin object) * refactor: use an enum for remote statuses --------- Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
This commit is contained in:
		
							parent
							
								
									e4f26a5e4b
								
							
						
					
					
						commit
						2fac3bd41e
					
				| 
						 | 
				
			
			@ -17,6 +17,7 @@ export default {
 | 
			
		|||
  isEnterprise: false,
 | 
			
		||||
  isInstalled: false,
 | 
			
		||||
  isDisabled: false,
 | 
			
		||||
  isDeprecated: false,
 | 
			
		||||
  isPublished: true,
 | 
			
		||||
  name: 'Zabbix',
 | 
			
		||||
  orgName: 'Alexander Zobnin',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime';
 | 
			
		|||
import { accessControlQueryParam } from 'app/core/utils/accessControl';
 | 
			
		||||
 | 
			
		||||
import { API_ROOT, GCOM_API_ROOT } from './constants';
 | 
			
		||||
import { isLocalPluginVisible, isRemotePluginVisible } from './helpers';
 | 
			
		||||
import { isLocalPluginVisibleByConfig, isRemotePluginVisibleByConfig } from './helpers';
 | 
			
		||||
import { LocalPlugin, RemotePlugin, CatalogPluginDetails, Version, PluginVersion } from './types';
 | 
			
		||||
 | 
			
		||||
export async function getPluginDetails(id: string): Promise<CatalogPluginDetails> {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,9 +29,13 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export async function getRemotePlugins(): Promise<RemotePlugin[]> {
 | 
			
		||||
  const { items: remotePlugins }: { items: RemotePlugin[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins`);
 | 
			
		||||
  // We are also fetching deprecated plugins, because we would like to be able to label plugins in the list that are both installed and deprecated.
 | 
			
		||||
  // (We won't show not installed deprecated plugins in the list)
 | 
			
		||||
  const { items: remotePlugins }: { items: RemotePlugin[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins`, {
 | 
			
		||||
    includeDeprecated: true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return remotePlugins.filter(isRemotePluginVisible);
 | 
			
		||||
  return remotePlugins.filter(isRemotePluginVisibleByConfig);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getPluginErrors(): Promise<PluginError[]> {
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +101,7 @@ export async function getLocalPlugins(): Promise<LocalPlugin[]> {
 | 
			
		|||
    accessControlQueryParam({ embedded: 0 })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return localPlugins.filter(isLocalPluginVisible);
 | 
			
		||||
  return localPlugins.filter(isLocalPluginVisibleByConfig);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function installPlugin(id: string) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { Badge } from '@grafana/ui';
 | 
			
		||||
 | 
			
		||||
export function PluginDeprecatedBadge(): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Badge
 | 
			
		||||
      icon="exclamation-triangle"
 | 
			
		||||
      text="Deprecated"
 | 
			
		||||
      color="orange"
 | 
			
		||||
      tooltip="This plugin is deprecated and no longer receives updates."
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,3 +3,4 @@ export { PluginInstalledBadge } from './PluginInstallBadge';
 | 
			
		|||
export { PluginEnterpriseBadge } from './PluginEnterpriseBadge';
 | 
			
		||||
export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge';
 | 
			
		||||
export { PluginAngularBadge } from './PluginAngularBadge';
 | 
			
		||||
export { PluginDeprecatedBadge } from './PluginDeprecatedBadge';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ const plugin: CatalogPlugin = {
 | 
			
		|||
  isDev: false,
 | 
			
		||||
  isEnterprise: false,
 | 
			
		||||
  isDisabled: false,
 | 
			
		||||
  isDeprecated: false,
 | 
			
		||||
  isPublished: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import React, { useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { Alert } from '@grafana/ui';
 | 
			
		||||
 | 
			
		||||
import { CatalogPlugin } from '../types';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  plugin: CatalogPlugin;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function PluginDetailsDeprecatedWarning(props: Props): React.ReactElement | null {
 | 
			
		||||
  const { className, plugin } = props;
 | 
			
		||||
  const [dismissed, setDismissed] = useState(false);
 | 
			
		||||
  const isWarningVisible = plugin.isDeprecated && !dismissed;
 | 
			
		||||
 | 
			
		||||
  return isWarningVisible ? (
 | 
			
		||||
    <Alert severity="warning" title="Deprecated" className={className} onRemove={() => setDismissed(true)}>
 | 
			
		||||
      <p>
 | 
			
		||||
        This {plugin.type} plugin is deprecated and removed from the catalog. No further updates will be made to the
 | 
			
		||||
        plugin.
 | 
			
		||||
      </p>
 | 
			
		||||
    </Alert>
 | 
			
		||||
  ) : null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +19,8 @@ import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';
 | 
			
		|||
import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';
 | 
			
		||||
import { PluginTabIds } from '../types';
 | 
			
		||||
 | 
			
		||||
import { PluginDetailsDeprecatedWarning } from './PluginDetailsDeprecatedWarning';
 | 
			
		||||
 | 
			
		||||
export type Props = {
 | 
			
		||||
  // The ID of the plugin
 | 
			
		||||
  pluginId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +89,7 @@ export function PluginDetailsPage({
 | 
			
		|||
          )}
 | 
			
		||||
          <PluginDetailsSignature plugin={plugin} className={styles.alert} />
 | 
			
		||||
          <PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
 | 
			
		||||
          <PluginDetailsDeprecatedWarning plugin={plugin} className={styles.alert} />
 | 
			
		||||
          <PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} />
 | 
			
		||||
        </TabContent>
 | 
			
		||||
      </Page.Contents>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ const getMockPlugin = (id: string): CatalogPlugin => {
 | 
			
		|||
    isDev: false,
 | 
			
		||||
    isEnterprise: false,
 | 
			
		||||
    isDisabled: false,
 | 
			
		||||
    isDeprecated: false,
 | 
			
		||||
    isPublished: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,6 +55,7 @@ describe('PluginListItem', () => {
 | 
			
		|||
    isDev: false,
 | 
			
		||||
    isEnterprise: false,
 | 
			
		||||
    isDisabled: false,
 | 
			
		||||
    isDeprecated: false,
 | 
			
		||||
    isPublished: true,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ describe('PluginListItemBadges', () => {
 | 
			
		|||
    isDev: false,
 | 
			
		||||
    isEnterprise: false,
 | 
			
		||||
    isDisabled: false,
 | 
			
		||||
    isDeprecated: false,
 | 
			
		||||
    isPublished: true,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import {
 | 
			
		|||
  PluginInstalledBadge,
 | 
			
		||||
  PluginUpdateAvailableBadge,
 | 
			
		||||
  PluginAngularBadge,
 | 
			
		||||
  PluginDeprecatedBadge,
 | 
			
		||||
} from './Badges';
 | 
			
		||||
 | 
			
		||||
type PluginBadgeType = {
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +36,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) {
 | 
			
		|||
    <HorizontalGroup height="auto" wrap>
 | 
			
		||||
      <PluginSignatureBadge status={plugin.signature} />
 | 
			
		||||
      {plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />}
 | 
			
		||||
      {plugin.isDeprecated && <PluginDeprecatedBadge />}
 | 
			
		||||
      {plugin.isInstalled && <PluginInstalledBadge />}
 | 
			
		||||
      {hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />}
 | 
			
		||||
      {plugin.angularDetected && <PluginAngularBadge />}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,10 +10,10 @@ import {
 | 
			
		|||
  mergeLocalsAndRemotes,
 | 
			
		||||
  sortPlugins,
 | 
			
		||||
  Sorters,
 | 
			
		||||
  isLocalPluginVisible,
 | 
			
		||||
  isRemotePluginVisible,
 | 
			
		||||
  isLocalPluginVisibleByConfig,
 | 
			
		||||
  isRemotePluginVisibleByConfig,
 | 
			
		||||
} from './helpers';
 | 
			
		||||
import { RemotePlugin, LocalPlugin } from './types';
 | 
			
		||||
import { RemotePlugin, LocalPlugin, RemotePluginStatus } from './types';
 | 
			
		||||
 | 
			
		||||
describe('Plugins/Helpers', () => {
 | 
			
		||||
  let remotePlugin: RemotePlugin;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +65,29 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
      // Only remote
 | 
			
		||||
      expect(findMerged('plugin-4')).toEqual(mergeLocalAndRemote(undefined, getRemotePluginMock({ slug: 'plugin-4' })));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('skips deprecated plugins unless they have a local - installed - counterpart', () => {
 | 
			
		||||
      const merged = mergeLocalsAndRemotes(localPlugins, [
 | 
			
		||||
        ...remotePlugins,
 | 
			
		||||
        getRemotePluginMock({ slug: 'plugin-5', status: RemotePluginStatus.Deprecated }),
 | 
			
		||||
      ]);
 | 
			
		||||
      const findMerged = (mergedId: string) => merged.find(({ id }) => id === mergedId);
 | 
			
		||||
 | 
			
		||||
      expect(merged).toHaveLength(4);
 | 
			
		||||
      expect(findMerged('plugin-5')).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('keeps deprecated plugins in case they have a local counterpart', () => {
 | 
			
		||||
      const merged = mergeLocalsAndRemotes(
 | 
			
		||||
        [...localPlugins, getLocalPluginMock({ id: 'plugin-5' })],
 | 
			
		||||
        [...remotePlugins, getRemotePluginMock({ slug: 'plugin-5', status: RemotePluginStatus.Deprecated })]
 | 
			
		||||
      );
 | 
			
		||||
      const findMerged = (mergedId: string) => merged.find(({ id }) => id === mergedId);
 | 
			
		||||
 | 
			
		||||
      expect(merged).toHaveLength(5);
 | 
			
		||||
      expect(findMerged('plugin-5')).not.toBeUndefined();
 | 
			
		||||
      expect(findMerged('plugin-5')?.isDeprecated).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('mergeLocalAndRemote()', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +123,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        isDisabled: false,
 | 
			
		||||
        isEnterprise: false,
 | 
			
		||||
        isInstalled: false,
 | 
			
		||||
        isDeprecated: false,
 | 
			
		||||
        isPublished: true,
 | 
			
		||||
        name: 'Zabbix',
 | 
			
		||||
        orgName: 'Alexander Zobnin',
 | 
			
		||||
| 
						 | 
				
			
			@ -139,8 +163,8 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    test('adds an "isEnterprise" field', () => {
 | 
			
		||||
      const enterprisePlugin = { ...remotePlugin, status: 'enterprise' } as RemotePlugin;
 | 
			
		||||
      const notEnterprisePlugin = { ...remotePlugin, status: 'unknown' } as RemotePlugin;
 | 
			
		||||
      const enterprisePlugin = { ...remotePlugin, status: RemotePluginStatus.Enterprise } as RemotePlugin;
 | 
			
		||||
      const notEnterprisePlugin = { ...remotePlugin, status: RemotePluginStatus.Active } as RemotePlugin;
 | 
			
		||||
 | 
			
		||||
      expect(mapRemoteToCatalog(enterprisePlugin).isEnterprise).toBe(true);
 | 
			
		||||
      expect(mapRemoteToCatalog(notEnterprisePlugin).isEnterprise).toBe(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +199,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        isEnterprise: false,
 | 
			
		||||
        isInstalled: true,
 | 
			
		||||
        isPublished: false,
 | 
			
		||||
        isDeprecated: false,
 | 
			
		||||
        name: 'Zabbix',
 | 
			
		||||
        orgName: 'Alexander Zobnin',
 | 
			
		||||
        popularity: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -223,6 +248,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        isEnterprise: false,
 | 
			
		||||
        isInstalled: true,
 | 
			
		||||
        isPublished: true,
 | 
			
		||||
        isDeprecated: false,
 | 
			
		||||
        name: 'Zabbix',
 | 
			
		||||
        orgName: 'Alexander Zobnin',
 | 
			
		||||
        popularity: 0.2111,
 | 
			
		||||
| 
						 | 
				
			
			@ -319,15 +345,17 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
 | 
			
		||||
    test('`.isEnterprise` - prefers the remote', () => {
 | 
			
		||||
      // Local & Remote
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'enterprise' })).toMatchObject({
 | 
			
		||||
        isEnterprise: true,
 | 
			
		||||
      });
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'unknown' })).toMatchObject({
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject(
 | 
			
		||||
        {
 | 
			
		||||
          isEnterprise: true,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Active })).toMatchObject({
 | 
			
		||||
        isEnterprise: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Remote only
 | 
			
		||||
      expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: 'enterprise' })).toMatchObject({
 | 
			
		||||
      expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject({
 | 
			
		||||
        isEnterprise: true,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -338,6 +366,34 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
      expect(mapToCatalogPlugin()).toMatchObject({ isEnterprise: false });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('`.isDeprecated` - comes from the remote', () => {
 | 
			
		||||
      // Local & Remote
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Deprecated })).toMatchObject(
 | 
			
		||||
        {
 | 
			
		||||
          isDeprecated: true,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject(
 | 
			
		||||
        {
 | 
			
		||||
          isDeprecated: false,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Remote only
 | 
			
		||||
      expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: RemotePluginStatus.Deprecated })).toMatchObject({
 | 
			
		||||
        isDeprecated: true,
 | 
			
		||||
      });
 | 
			
		||||
      expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject({
 | 
			
		||||
        isDeprecated: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Local only
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ isDeprecated: false });
 | 
			
		||||
 | 
			
		||||
      // No local or remote
 | 
			
		||||
      expect(mapToCatalogPlugin()).toMatchObject({ isDeprecated: false });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('`.isInstalled` - prefers the local', () => {
 | 
			
		||||
      // Local & Remote
 | 
			
		||||
      expect(mapToCatalogPlugin(localPlugin, remotePlugin)).toMatchObject({ isInstalled: true });
 | 
			
		||||
| 
						 | 
				
			
			@ -671,7 +727,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        id: 'barchart',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(isLocalPluginVisible(plugin)).toBe(true);
 | 
			
		||||
      expect(isLocalPluginVisibleByConfig(plugin)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should return FALSE if the plugin is listed as hidden in the main Grafana configuration', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -680,7 +736,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        id: 'akumuli-datasource',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(isLocalPluginVisible(plugin)).toBe(false);
 | 
			
		||||
      expect(isLocalPluginVisibleByConfig(plugin)).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -691,7 +747,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        slug: 'barchart',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(isRemotePluginVisible(plugin)).toBe(true);
 | 
			
		||||
      expect(isRemotePluginVisibleByConfig(plugin)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should return FALSE if the plugin is listed as hidden in the main Grafana configuration', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -700,7 +756,7 @@ describe('Plugins/Helpers', () => {
 | 
			
		|||
        slug: 'akumuli-datasource',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(isRemotePluginVisible(plugin)).toBe(false);
 | 
			
		||||
      expect(isRemotePluginVisibleByConfig(plugin)).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import { contextSrv } from 'app/core/core';
 | 
			
		|||
import { getBackendSrv } from 'app/core/services/backend_srv';
 | 
			
		||||
import { AccessControlAction } from 'app/types';
 | 
			
		||||
 | 
			
		||||
import { CatalogPlugin, LocalPlugin, RemotePlugin, Version } from './types';
 | 
			
		||||
import { CatalogPlugin, LocalPlugin, RemotePlugin, RemotePluginStatus, Version } from './types';
 | 
			
		||||
 | 
			
		||||
export function mergeLocalsAndRemotes(
 | 
			
		||||
  local: LocalPlugin[] = [],
 | 
			
		||||
| 
						 | 
				
			
			@ -16,21 +16,24 @@ export function mergeLocalsAndRemotes(
 | 
			
		|||
  const errorByPluginId = groupErrorsByPluginId(errors);
 | 
			
		||||
 | 
			
		||||
  // add locals
 | 
			
		||||
  local.forEach((l) => {
 | 
			
		||||
    const remotePlugin = remote.find((r) => r.slug === l.id);
 | 
			
		||||
    const error = errorByPluginId[l.id];
 | 
			
		||||
  local.forEach((localPlugin) => {
 | 
			
		||||
    const remoteCounterpart = remote.find((r) => r.slug === localPlugin.id);
 | 
			
		||||
    const error = errorByPluginId[localPlugin.id];
 | 
			
		||||
 | 
			
		||||
    if (!remotePlugin) {
 | 
			
		||||
      catalogPlugins.push(mergeLocalAndRemote(l, undefined, error));
 | 
			
		||||
    if (!remoteCounterpart) {
 | 
			
		||||
      catalogPlugins.push(mergeLocalAndRemote(localPlugin, undefined, error));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // add remote
 | 
			
		||||
  remote.forEach((r) => {
 | 
			
		||||
    const localPlugin = local.find((l) => l.id === r.slug);
 | 
			
		||||
    const error = errorByPluginId[r.slug];
 | 
			
		||||
  remote.forEach((remotePlugin) => {
 | 
			
		||||
    const localCounterpart = local.find((l) => l.id === remotePlugin.slug);
 | 
			
		||||
    const error = errorByPluginId[remotePlugin.slug];
 | 
			
		||||
    const shouldSkip = remotePlugin.status === RemotePluginStatus.Deprecated && !localCounterpart; // We are only listing deprecated plugins in case they are installed.
 | 
			
		||||
 | 
			
		||||
    catalogPlugins.push(mergeLocalAndRemote(localPlugin, r, error));
 | 
			
		||||
    if (!shouldSkip) {
 | 
			
		||||
      catalogPlugins.push(mergeLocalAndRemote(localCounterpart, remotePlugin, error));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return catalogPlugins;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,9 +88,10 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
 | 
			
		|||
    isPublished: true,
 | 
			
		||||
    isInstalled: isDisabled,
 | 
			
		||||
    isDisabled: isDisabled,
 | 
			
		||||
    isDeprecated: status === RemotePluginStatus.Deprecated,
 | 
			
		||||
    isCore: plugin.internal,
 | 
			
		||||
    isDev: false,
 | 
			
		||||
    isEnterprise: status === 'enterprise',
 | 
			
		||||
    isEnterprise: status === RemotePluginStatus.Enterprise,
 | 
			
		||||
    type: typeCode,
 | 
			
		||||
    error: error?.errorCode,
 | 
			
		||||
    angularDetected,
 | 
			
		||||
| 
						 | 
				
			
			@ -129,6 +133,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
 | 
			
		|||
    isDisabled: isDisabled,
 | 
			
		||||
    isCore: signature === 'internal',
 | 
			
		||||
    isPublished: false,
 | 
			
		||||
    isDeprecated: false,
 | 
			
		||||
    isDev: Boolean(dev),
 | 
			
		||||
    isEnterprise: false,
 | 
			
		||||
    type,
 | 
			
		||||
| 
						 | 
				
			
			@ -169,9 +174,10 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
 | 
			
		|||
    },
 | 
			
		||||
    isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
 | 
			
		||||
    isDev: Boolean(local?.dev),
 | 
			
		||||
    isEnterprise: remote?.status === 'enterprise',
 | 
			
		||||
    isEnterprise: remote?.status === RemotePluginStatus.Enterprise,
 | 
			
		||||
    isInstalled: Boolean(local) || isDisabled,
 | 
			
		||||
    isDisabled: isDisabled,
 | 
			
		||||
    isDeprecated: remote?.status === RemotePluginStatus.Deprecated,
 | 
			
		||||
    isPublished: true,
 | 
			
		||||
    // TODO<check if we would like to keep preferring the remote version>
 | 
			
		||||
    name: remote?.name || local?.name || '',
 | 
			
		||||
| 
						 | 
				
			
			@ -296,11 +302,11 @@ export const hasInstallControlWarning = (
 | 
			
		|||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
 | 
			
		||||
export const isLocalPluginVisibleByConfig = (p: LocalPlugin) => isNotHiddenByConfig(p.id);
 | 
			
		||||
 | 
			
		||||
export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
 | 
			
		||||
export const isRemotePluginVisibleByConfig = (p: RemotePlugin) => isNotHiddenByConfig(p.slug);
 | 
			
		||||
 | 
			
		||||
function isPluginVisible(id: string) {
 | 
			
		||||
function isNotHiddenByConfig(id: string) {
 | 
			
		||||
  const { pluginCatalogHiddenPlugins }: { pluginCatalogHiddenPlugins: string[] } = config;
 | 
			
		||||
 | 
			
		||||
  return !pluginCatalogHiddenPlugins.includes(id);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -743,6 +743,30 @@ describe('Plugin details page', () => {
 | 
			
		|||
 | 
			
		||||
      await waitFor(() => expect(queryByText(/angular plugin/i)).not.toBeInTheDocument);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should display a deprecation warning if the plugin is deprecated', async () => {
 | 
			
		||||
      const { queryByText } = renderPluginDetails({
 | 
			
		||||
        id,
 | 
			
		||||
        isInstalled: true,
 | 
			
		||||
        isDeprecated: true,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await waitFor(() =>
 | 
			
		||||
        expect(queryByText(/plugin is deprecated and removed from the catalog/i)).toBeInTheDocument()
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not display a deprecation warning in the plugin is not deprecated', async () => {
 | 
			
		||||
      const { queryByText } = renderPluginDetails({
 | 
			
		||||
        id,
 | 
			
		||||
        isInstalled: true,
 | 
			
		||||
        isDeprecated: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await waitFor(() =>
 | 
			
		||||
        expect(queryByText(/plugin is deprecated and removed from the catalog/i)).not.toBeInTheDocument()
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('viewed as user without grafana admin permissions', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
 | 
			
		|||
  isEnterprise: boolean;
 | 
			
		||||
  isInstalled: boolean;
 | 
			
		||||
  isDisabled: boolean;
 | 
			
		||||
  isDeprecated: boolean;
 | 
			
		||||
  // `isPublished` is TRUE if the plugin is published to grafana.com
 | 
			
		||||
  isPublished: boolean;
 | 
			
		||||
  name: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +112,7 @@ export type RemotePlugin = {
 | 
			
		|||
  readme?: string;
 | 
			
		||||
  signatureType: PluginSignatureType | '';
 | 
			
		||||
  slug: string;
 | 
			
		||||
  status: string;
 | 
			
		||||
  status: RemotePluginStatus;
 | 
			
		||||
  typeCode: PluginType;
 | 
			
		||||
  typeId: number;
 | 
			
		||||
  typeName: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +128,16 @@ export type RemotePlugin = {
 | 
			
		|||
  angularDetected?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// The available status codes on GCOM are available here:
 | 
			
		||||
// https://github.com/grafana/grafana-com/blob/main/packages/grafana-com-plugins-api/src/plugins/plugin.model.js#L74
 | 
			
		||||
export enum RemotePluginStatus {
 | 
			
		||||
  Deleted = 'deleted',
 | 
			
		||||
  Active = 'active',
 | 
			
		||||
  Pending = 'pending',
 | 
			
		||||
  Deprecated = 'deprecated',
 | 
			
		||||
  Enterprise = 'enterprise',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type LocalPlugin = WithAccessControlMetadata & {
 | 
			
		||||
  category: string;
 | 
			
		||||
  defaultNavUrl: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue