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, |   isEnterprise: false, | ||||||
|   isInstalled: false, |   isInstalled: false, | ||||||
|   isDisabled: false, |   isDisabled: false, | ||||||
|  |   isDeprecated: false, | ||||||
|   isPublished: true, |   isPublished: true, | ||||||
|   name: 'Zabbix', |   name: 'Zabbix', | ||||||
|   orgName: 'Alexander Zobnin', |   orgName: 'Alexander Zobnin', | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; | ||||||
| import { accessControlQueryParam } from 'app/core/utils/accessControl'; | import { accessControlQueryParam } from 'app/core/utils/accessControl'; | ||||||
| 
 | 
 | ||||||
| import { API_ROOT, GCOM_API_ROOT } from './constants'; | 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'; | import { LocalPlugin, RemotePlugin, CatalogPluginDetails, Version, PluginVersion } from './types'; | ||||||
| 
 | 
 | ||||||
| export async function getPluginDetails(id: string): Promise<CatalogPluginDetails> { | 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[]> { | 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[]> { | export async function getPluginErrors(): Promise<PluginError[]> { | ||||||
|  | @ -97,7 +101,7 @@ export async function getLocalPlugins(): Promise<LocalPlugin[]> { | ||||||
|     accessControlQueryParam({ embedded: 0 }) |     accessControlQueryParam({ embedded: 0 }) | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return localPlugins.filter(isLocalPluginVisible); |   return localPlugins.filter(isLocalPluginVisibleByConfig); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function installPlugin(id: string) { | 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 { PluginEnterpriseBadge } from './PluginEnterpriseBadge'; | ||||||
| export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge'; | export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge'; | ||||||
| export { PluginAngularBadge } from './PluginAngularBadge'; | export { PluginAngularBadge } from './PluginAngularBadge'; | ||||||
|  | export { PluginDeprecatedBadge } from './PluginDeprecatedBadge'; | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ const plugin: CatalogPlugin = { | ||||||
|   isDev: false, |   isDev: false, | ||||||
|   isEnterprise: false, |   isEnterprise: false, | ||||||
|   isDisabled: false, |   isDisabled: false, | ||||||
|  |   isDeprecated: false, | ||||||
|   isPublished: true, |   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 { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks'; | ||||||
| import { PluginTabIds } from '../types'; | import { PluginTabIds } from '../types'; | ||||||
| 
 | 
 | ||||||
|  | import { PluginDetailsDeprecatedWarning } from './PluginDetailsDeprecatedWarning'; | ||||||
|  | 
 | ||||||
| export type Props = { | export type Props = { | ||||||
|   // The ID of the plugin
 |   // The ID of the plugin
 | ||||||
|   pluginId: string; |   pluginId: string; | ||||||
|  | @ -87,6 +89,7 @@ export function PluginDetailsPage({ | ||||||
|           )} |           )} | ||||||
|           <PluginDetailsSignature plugin={plugin} className={styles.alert} /> |           <PluginDetailsSignature plugin={plugin} className={styles.alert} /> | ||||||
|           <PluginDetailsDisabledError 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} /> |           <PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} /> | ||||||
|         </TabContent> |         </TabContent> | ||||||
|       </Page.Contents> |       </Page.Contents> | ||||||
|  |  | ||||||
|  | @ -45,6 +45,7 @@ const getMockPlugin = (id: string): CatalogPlugin => { | ||||||
|     isDev: false, |     isDev: false, | ||||||
|     isEnterprise: false, |     isEnterprise: false, | ||||||
|     isDisabled: false, |     isDisabled: false, | ||||||
|  |     isDeprecated: false, | ||||||
|     isPublished: true, |     isPublished: true, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -55,6 +55,7 @@ describe('PluginListItem', () => { | ||||||
|     isDev: false, |     isDev: false, | ||||||
|     isEnterprise: false, |     isEnterprise: false, | ||||||
|     isDisabled: false, |     isDisabled: false, | ||||||
|  |     isDeprecated: false, | ||||||
|     isPublished: true, |     isPublished: true, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31,6 +31,7 @@ describe('PluginListItemBadges', () => { | ||||||
|     isDev: false, |     isDev: false, | ||||||
|     isEnterprise: false, |     isEnterprise: false, | ||||||
|     isDisabled: false, |     isDisabled: false, | ||||||
|  |     isDeprecated: false, | ||||||
|     isPublished: true, |     isPublished: true, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import { | ||||||
|   PluginInstalledBadge, |   PluginInstalledBadge, | ||||||
|   PluginUpdateAvailableBadge, |   PluginUpdateAvailableBadge, | ||||||
|   PluginAngularBadge, |   PluginAngularBadge, | ||||||
|  |   PluginDeprecatedBadge, | ||||||
| } from './Badges'; | } from './Badges'; | ||||||
| 
 | 
 | ||||||
| type PluginBadgeType = { | type PluginBadgeType = { | ||||||
|  | @ -35,6 +36,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { | ||||||
|     <HorizontalGroup height="auto" wrap> |     <HorizontalGroup height="auto" wrap> | ||||||
|       <PluginSignatureBadge status={plugin.signature} /> |       <PluginSignatureBadge status={plugin.signature} /> | ||||||
|       {plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />} |       {plugin.isDisabled && <PluginDisabledBadge error={plugin.error} />} | ||||||
|  |       {plugin.isDeprecated && <PluginDeprecatedBadge />} | ||||||
|       {plugin.isInstalled && <PluginInstalledBadge />} |       {plugin.isInstalled && <PluginInstalledBadge />} | ||||||
|       {hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />} |       {hasUpdate && <PluginUpdateAvailableBadge plugin={plugin} />} | ||||||
|       {plugin.angularDetected && <PluginAngularBadge />} |       {plugin.angularDetected && <PluginAngularBadge />} | ||||||
|  |  | ||||||
|  | @ -10,10 +10,10 @@ import { | ||||||
|   mergeLocalsAndRemotes, |   mergeLocalsAndRemotes, | ||||||
|   sortPlugins, |   sortPlugins, | ||||||
|   Sorters, |   Sorters, | ||||||
|   isLocalPluginVisible, |   isLocalPluginVisibleByConfig, | ||||||
|   isRemotePluginVisible, |   isRemotePluginVisibleByConfig, | ||||||
| } from './helpers'; | } from './helpers'; | ||||||
| import { RemotePlugin, LocalPlugin } from './types'; | import { RemotePlugin, LocalPlugin, RemotePluginStatus } from './types'; | ||||||
| 
 | 
 | ||||||
| describe('Plugins/Helpers', () => { | describe('Plugins/Helpers', () => { | ||||||
|   let remotePlugin: RemotePlugin; |   let remotePlugin: RemotePlugin; | ||||||
|  | @ -65,6 +65,29 @@ describe('Plugins/Helpers', () => { | ||||||
|       // Only remote
 |       // Only remote
 | ||||||
|       expect(findMerged('plugin-4')).toEqual(mergeLocalAndRemote(undefined, getRemotePluginMock({ slug: 'plugin-4' }))); |       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()', () => { |   describe('mergeLocalAndRemote()', () => { | ||||||
|  | @ -100,6 +123,7 @@ describe('Plugins/Helpers', () => { | ||||||
|         isDisabled: false, |         isDisabled: false, | ||||||
|         isEnterprise: false, |         isEnterprise: false, | ||||||
|         isInstalled: false, |         isInstalled: false, | ||||||
|  |         isDeprecated: false, | ||||||
|         isPublished: true, |         isPublished: true, | ||||||
|         name: 'Zabbix', |         name: 'Zabbix', | ||||||
|         orgName: 'Alexander Zobnin', |         orgName: 'Alexander Zobnin', | ||||||
|  | @ -139,8 +163,8 @@ describe('Plugins/Helpers', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('adds an "isEnterprise" field', () => { |     test('adds an "isEnterprise" field', () => { | ||||||
|       const enterprisePlugin = { ...remotePlugin, status: 'enterprise' } as RemotePlugin; |       const enterprisePlugin = { ...remotePlugin, status: RemotePluginStatus.Enterprise } as RemotePlugin; | ||||||
|       const notEnterprisePlugin = { ...remotePlugin, status: 'unknown' } as RemotePlugin; |       const notEnterprisePlugin = { ...remotePlugin, status: RemotePluginStatus.Active } as RemotePlugin; | ||||||
| 
 | 
 | ||||||
|       expect(mapRemoteToCatalog(enterprisePlugin).isEnterprise).toBe(true); |       expect(mapRemoteToCatalog(enterprisePlugin).isEnterprise).toBe(true); | ||||||
|       expect(mapRemoteToCatalog(notEnterprisePlugin).isEnterprise).toBe(false); |       expect(mapRemoteToCatalog(notEnterprisePlugin).isEnterprise).toBe(false); | ||||||
|  | @ -175,6 +199,7 @@ describe('Plugins/Helpers', () => { | ||||||
|         isEnterprise: false, |         isEnterprise: false, | ||||||
|         isInstalled: true, |         isInstalled: true, | ||||||
|         isPublished: false, |         isPublished: false, | ||||||
|  |         isDeprecated: false, | ||||||
|         name: 'Zabbix', |         name: 'Zabbix', | ||||||
|         orgName: 'Alexander Zobnin', |         orgName: 'Alexander Zobnin', | ||||||
|         popularity: 0, |         popularity: 0, | ||||||
|  | @ -223,6 +248,7 @@ describe('Plugins/Helpers', () => { | ||||||
|         isEnterprise: false, |         isEnterprise: false, | ||||||
|         isInstalled: true, |         isInstalled: true, | ||||||
|         isPublished: true, |         isPublished: true, | ||||||
|  |         isDeprecated: false, | ||||||
|         name: 'Zabbix', |         name: 'Zabbix', | ||||||
|         orgName: 'Alexander Zobnin', |         orgName: 'Alexander Zobnin', | ||||||
|         popularity: 0.2111, |         popularity: 0.2111, | ||||||
|  | @ -319,15 +345,17 @@ describe('Plugins/Helpers', () => { | ||||||
| 
 | 
 | ||||||
|     test('`.isEnterprise` - prefers the remote', () => { |     test('`.isEnterprise` - prefers the remote', () => { | ||||||
|       // Local & Remote
 |       // Local & Remote
 | ||||||
|       expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'enterprise' })).toMatchObject({ |       expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject( | ||||||
|         isEnterprise: true, |         { | ||||||
|       }); |           isEnterprise: true, | ||||||
|       expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'unknown' })).toMatchObject({ |         } | ||||||
|  |       ); | ||||||
|  |       expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: RemotePluginStatus.Active })).toMatchObject({ | ||||||
|         isEnterprise: false, |         isEnterprise: false, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       // Remote only
 |       // Remote only
 | ||||||
|       expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: 'enterprise' })).toMatchObject({ |       expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: RemotePluginStatus.Enterprise })).toMatchObject({ | ||||||
|         isEnterprise: true, |         isEnterprise: true, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -338,6 +366,34 @@ describe('Plugins/Helpers', () => { | ||||||
|       expect(mapToCatalogPlugin()).toMatchObject({ isEnterprise: false }); |       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', () => { |     test('`.isInstalled` - prefers the local', () => { | ||||||
|       // Local & Remote
 |       // Local & Remote
 | ||||||
|       expect(mapToCatalogPlugin(localPlugin, remotePlugin)).toMatchObject({ isInstalled: true }); |       expect(mapToCatalogPlugin(localPlugin, remotePlugin)).toMatchObject({ isInstalled: true }); | ||||||
|  | @ -671,7 +727,7 @@ describe('Plugins/Helpers', () => { | ||||||
|         id: 'barchart', |         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', () => { |     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', |         id: 'akumuli-datasource', | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       expect(isLocalPluginVisible(plugin)).toBe(false); |       expect(isLocalPluginVisibleByConfig(plugin)).toBe(false); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | @ -691,7 +747,7 @@ describe('Plugins/Helpers', () => { | ||||||
|         slug: 'barchart', |         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', () => { |     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', |         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 { getBackendSrv } from 'app/core/services/backend_srv'; | ||||||
| import { AccessControlAction } from 'app/types'; | import { AccessControlAction } from 'app/types'; | ||||||
| 
 | 
 | ||||||
| import { CatalogPlugin, LocalPlugin, RemotePlugin, Version } from './types'; | import { CatalogPlugin, LocalPlugin, RemotePlugin, RemotePluginStatus, Version } from './types'; | ||||||
| 
 | 
 | ||||||
| export function mergeLocalsAndRemotes( | export function mergeLocalsAndRemotes( | ||||||
|   local: LocalPlugin[] = [], |   local: LocalPlugin[] = [], | ||||||
|  | @ -16,21 +16,24 @@ export function mergeLocalsAndRemotes( | ||||||
|   const errorByPluginId = groupErrorsByPluginId(errors); |   const errorByPluginId = groupErrorsByPluginId(errors); | ||||||
| 
 | 
 | ||||||
|   // add locals
 |   // add locals
 | ||||||
|   local.forEach((l) => { |   local.forEach((localPlugin) => { | ||||||
|     const remotePlugin = remote.find((r) => r.slug === l.id); |     const remoteCounterpart = remote.find((r) => r.slug === localPlugin.id); | ||||||
|     const error = errorByPluginId[l.id]; |     const error = errorByPluginId[localPlugin.id]; | ||||||
| 
 | 
 | ||||||
|     if (!remotePlugin) { |     if (!remoteCounterpart) { | ||||||
|       catalogPlugins.push(mergeLocalAndRemote(l, undefined, error)); |       catalogPlugins.push(mergeLocalAndRemote(localPlugin, undefined, error)); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   // add remote
 |   // add remote
 | ||||||
|   remote.forEach((r) => { |   remote.forEach((remotePlugin) => { | ||||||
|     const localPlugin = local.find((l) => l.id === r.slug); |     const localCounterpart = local.find((l) => l.id === remotePlugin.slug); | ||||||
|     const error = errorByPluginId[r.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; |   return catalogPlugins; | ||||||
|  | @ -85,9 +88,10 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C | ||||||
|     isPublished: true, |     isPublished: true, | ||||||
|     isInstalled: isDisabled, |     isInstalled: isDisabled, | ||||||
|     isDisabled: isDisabled, |     isDisabled: isDisabled, | ||||||
|  |     isDeprecated: status === RemotePluginStatus.Deprecated, | ||||||
|     isCore: plugin.internal, |     isCore: plugin.internal, | ||||||
|     isDev: false, |     isDev: false, | ||||||
|     isEnterprise: status === 'enterprise', |     isEnterprise: status === RemotePluginStatus.Enterprise, | ||||||
|     type: typeCode, |     type: typeCode, | ||||||
|     error: error?.errorCode, |     error: error?.errorCode, | ||||||
|     angularDetected, |     angularDetected, | ||||||
|  | @ -129,6 +133,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat | ||||||
|     isDisabled: isDisabled, |     isDisabled: isDisabled, | ||||||
|     isCore: signature === 'internal', |     isCore: signature === 'internal', | ||||||
|     isPublished: false, |     isPublished: false, | ||||||
|  |     isDeprecated: false, | ||||||
|     isDev: Boolean(dev), |     isDev: Boolean(dev), | ||||||
|     isEnterprise: false, |     isEnterprise: false, | ||||||
|     type, |     type, | ||||||
|  | @ -169,9 +174,10 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e | ||||||
|     }, |     }, | ||||||
|     isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal), |     isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal), | ||||||
|     isDev: Boolean(local?.dev), |     isDev: Boolean(local?.dev), | ||||||
|     isEnterprise: remote?.status === 'enterprise', |     isEnterprise: remote?.status === RemotePluginStatus.Enterprise, | ||||||
|     isInstalled: Boolean(local) || isDisabled, |     isInstalled: Boolean(local) || isDisabled, | ||||||
|     isDisabled: isDisabled, |     isDisabled: isDisabled, | ||||||
|  |     isDeprecated: remote?.status === RemotePluginStatus.Deprecated, | ||||||
|     isPublished: true, |     isPublished: true, | ||||||
|     // TODO<check if we would like to keep preferring the remote version>
 |     // TODO<check if we would like to keep preferring the remote version>
 | ||||||
|     name: remote?.name || local?.name || '', |     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; |   const { pluginCatalogHiddenPlugins }: { pluginCatalogHiddenPlugins: string[] } = config; | ||||||
| 
 | 
 | ||||||
|   return !pluginCatalogHiddenPlugins.includes(id); |   return !pluginCatalogHiddenPlugins.includes(id); | ||||||
|  |  | ||||||
|  | @ -743,6 +743,30 @@ describe('Plugin details page', () => { | ||||||
| 
 | 
 | ||||||
|       await waitFor(() => expect(queryByText(/angular plugin/i)).not.toBeInTheDocument); |       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', () => { |   describe('viewed as user without grafana admin permissions', () => { | ||||||
|  |  | ||||||
|  | @ -43,6 +43,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata { | ||||||
|   isEnterprise: boolean; |   isEnterprise: boolean; | ||||||
|   isInstalled: boolean; |   isInstalled: boolean; | ||||||
|   isDisabled: boolean; |   isDisabled: boolean; | ||||||
|  |   isDeprecated: boolean; | ||||||
|   // `isPublished` is TRUE if the plugin is published to grafana.com
 |   // `isPublished` is TRUE if the plugin is published to grafana.com
 | ||||||
|   isPublished: boolean; |   isPublished: boolean; | ||||||
|   name: string; |   name: string; | ||||||
|  | @ -111,7 +112,7 @@ export type RemotePlugin = { | ||||||
|   readme?: string; |   readme?: string; | ||||||
|   signatureType: PluginSignatureType | ''; |   signatureType: PluginSignatureType | ''; | ||||||
|   slug: string; |   slug: string; | ||||||
|   status: string; |   status: RemotePluginStatus; | ||||||
|   typeCode: PluginType; |   typeCode: PluginType; | ||||||
|   typeId: number; |   typeId: number; | ||||||
|   typeName: string; |   typeName: string; | ||||||
|  | @ -127,6 +128,16 @@ export type RemotePlugin = { | ||||||
|   angularDetected?: boolean; |   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 & { | export type LocalPlugin = WithAccessControlMetadata & { | ||||||
|   category: string; |   category: string; | ||||||
|   defaultNavUrl: string; |   defaultNavUrl: string; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue