helpers.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import { PluginSignatureStatus, dateTimeParse, PluginError, PluginErrorCode } from '@grafana/data';
  2. import { config } from '@grafana/runtime';
  3. import { Settings } from 'app/core/config';
  4. import { getBackendSrv } from 'app/core/services/backend_srv';
  5. import { CatalogPlugin, LocalPlugin, RemotePlugin, Version } from './types';
  6. export function mergeLocalsAndRemotes(
  7. local: LocalPlugin[] = [],
  8. remote: RemotePlugin[] = [],
  9. errors?: PluginError[]
  10. ): CatalogPlugin[] {
  11. const catalogPlugins: CatalogPlugin[] = [];
  12. const errorByPluginId = groupErrorsByPluginId(errors);
  13. // add locals
  14. local.forEach((l) => {
  15. const remotePlugin = remote.find((r) => r.slug === l.id);
  16. const error = errorByPluginId[l.id];
  17. if (!remotePlugin) {
  18. catalogPlugins.push(mergeLocalAndRemote(l, undefined, error));
  19. }
  20. });
  21. // add remote
  22. remote.forEach((r) => {
  23. const localPlugin = local.find((l) => l.id === r.slug);
  24. const error = errorByPluginId[r.slug];
  25. catalogPlugins.push(mergeLocalAndRemote(localPlugin, r, error));
  26. });
  27. return catalogPlugins;
  28. }
  29. export function mergeLocalAndRemote(local?: LocalPlugin, remote?: RemotePlugin, error?: PluginError): CatalogPlugin {
  30. if (!local && remote) {
  31. return mapRemoteToCatalog(remote, error);
  32. }
  33. if (local && !remote) {
  34. return mapLocalToCatalog(local, error);
  35. }
  36. return mapToCatalogPlugin(local, remote, error);
  37. }
  38. export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): CatalogPlugin {
  39. const {
  40. name,
  41. slug: id,
  42. description,
  43. version,
  44. orgName,
  45. popularity,
  46. downloads,
  47. typeCode,
  48. updatedAt,
  49. createdAt: publishedAt,
  50. status,
  51. } = plugin;
  52. const isDisabled = !!error;
  53. return {
  54. description,
  55. downloads,
  56. id,
  57. info: {
  58. logos: {
  59. small: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/small`,
  60. large: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/large`,
  61. },
  62. },
  63. name,
  64. orgName,
  65. popularity,
  66. publishedAt,
  67. signature: getPluginSignature({ remote: plugin, error }),
  68. updatedAt,
  69. hasUpdate: false,
  70. isPublished: true,
  71. isInstalled: isDisabled,
  72. isDisabled: isDisabled,
  73. isCore: plugin.internal,
  74. isDev: false,
  75. isEnterprise: status === 'enterprise',
  76. type: typeCode,
  77. error: error?.errorCode,
  78. };
  79. }
  80. export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): CatalogPlugin {
  81. const {
  82. name,
  83. info: { description, version, logos, updated, author },
  84. id,
  85. dev,
  86. type,
  87. signature,
  88. signatureOrg,
  89. signatureType,
  90. hasUpdate,
  91. } = plugin;
  92. return {
  93. description,
  94. downloads: 0,
  95. id,
  96. info: { logos },
  97. name,
  98. orgName: author.name,
  99. popularity: 0,
  100. publishedAt: '',
  101. signature: getPluginSignature({ local: plugin, error }),
  102. signatureOrg,
  103. signatureType,
  104. updatedAt: updated,
  105. installedVersion: version,
  106. hasUpdate,
  107. isInstalled: true,
  108. isDisabled: !!error,
  109. isCore: signature === 'internal',
  110. isPublished: false,
  111. isDev: Boolean(dev),
  112. isEnterprise: false,
  113. type,
  114. error: error?.errorCode,
  115. };
  116. }
  117. // TODO: change the signature by removing the optionals for local and remote.
  118. export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, error?: PluginError): CatalogPlugin {
  119. const installedVersion = local?.info.version;
  120. const id = remote?.slug || local?.id || '';
  121. const type = local?.type || remote?.typeCode;
  122. const isDisabled = !!error;
  123. let logos = {
  124. small: `/public/img/icn-${type}.svg`,
  125. large: `/public/img/icn-${type}.svg`,
  126. };
  127. if (remote) {
  128. logos = {
  129. small: `https://grafana.com/api/plugins/${id}/versions/${remote.version}/logos/small`,
  130. large: `https://grafana.com/api/plugins/${id}/versions/${remote.version}/logos/large`,
  131. };
  132. } else if (local && local.info.logos) {
  133. logos = local.info.logos;
  134. }
  135. return {
  136. description: local?.info.description || remote?.description || '',
  137. downloads: remote?.downloads || 0,
  138. hasUpdate: local?.hasUpdate || false,
  139. id,
  140. info: {
  141. logos,
  142. },
  143. isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
  144. isDev: Boolean(local?.dev),
  145. isEnterprise: remote?.status === 'enterprise',
  146. isInstalled: Boolean(local) || isDisabled,
  147. isDisabled: isDisabled,
  148. isPublished: true,
  149. // TODO<check if we would like to keep preferring the remote version>
  150. name: remote?.name || local?.name || '',
  151. // TODO<check if we would like to keep preferring the remote version>
  152. orgName: remote?.orgName || local?.info.author.name || '',
  153. popularity: remote?.popularity || 0,
  154. publishedAt: remote?.createdAt || '',
  155. type,
  156. signature: getPluginSignature({ local, remote, error }),
  157. signatureOrg: local?.signatureOrg || remote?.versionSignedByOrgName,
  158. signatureType: local?.signatureType || remote?.versionSignatureType || remote?.signatureType || undefined,
  159. // TODO<check if we would like to keep preferring the remote version>
  160. updatedAt: remote?.updatedAt || local?.info.updated || '',
  161. installedVersion,
  162. error: error?.errorCode,
  163. };
  164. }
  165. export const getExternalManageLink = (pluginId: string) => `${config.pluginCatalogURL}${pluginId}`;
  166. export enum Sorters {
  167. nameAsc = 'nameAsc',
  168. nameDesc = 'nameDesc',
  169. updated = 'updated',
  170. published = 'published',
  171. downloads = 'downloads',
  172. }
  173. export const sortPlugins = (plugins: CatalogPlugin[], sortBy: Sorters) => {
  174. const sorters: { [name: string]: (a: CatalogPlugin, b: CatalogPlugin) => number } = {
  175. nameAsc: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name),
  176. nameDesc: (a: CatalogPlugin, b: CatalogPlugin) => b.name.localeCompare(a.name),
  177. updated: (a: CatalogPlugin, b: CatalogPlugin) =>
  178. dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(),
  179. published: (a: CatalogPlugin, b: CatalogPlugin) =>
  180. dateTimeParse(b.publishedAt).valueOf() - dateTimeParse(a.publishedAt).valueOf(),
  181. downloads: (a: CatalogPlugin, b: CatalogPlugin) => b.downloads - a.downloads,
  182. };
  183. if (sorters[sortBy]) {
  184. return plugins.sort(sorters[sortBy]);
  185. }
  186. return plugins;
  187. };
  188. function groupErrorsByPluginId(errors: PluginError[] = []): Record<string, PluginError | undefined> {
  189. return errors.reduce((byId, error) => {
  190. byId[error.pluginId] = error;
  191. return byId;
  192. }, {} as Record<string, PluginError | undefined>);
  193. }
  194. function getPluginSignature(options: {
  195. local?: LocalPlugin;
  196. remote?: RemotePlugin;
  197. error?: PluginError;
  198. }): PluginSignatureStatus {
  199. const { error, local, remote } = options;
  200. if (error) {
  201. switch (error.errorCode) {
  202. case PluginErrorCode.invalidSignature:
  203. return PluginSignatureStatus.invalid;
  204. case PluginErrorCode.missingSignature:
  205. return PluginSignatureStatus.missing;
  206. case PluginErrorCode.modifiedSignature:
  207. return PluginSignatureStatus.modified;
  208. }
  209. }
  210. if (local?.signature) {
  211. return local.signature;
  212. }
  213. if (remote?.signatureType || remote?.versionSignatureType) {
  214. return PluginSignatureStatus.valid;
  215. }
  216. return PluginSignatureStatus.missing;
  217. }
  218. // Updates the core Grafana config to have the correct list available panels
  219. export const updatePanels = () =>
  220. getBackendSrv()
  221. .get('/api/frontend/settings')
  222. .then((settings: Settings) => {
  223. config.panels = settings.panels;
  224. });
  225. export function getLatestCompatibleVersion(versions: Version[] | undefined): Version | undefined {
  226. if (!versions) {
  227. return;
  228. }
  229. const [latest] = versions.filter((v) => Boolean(v.isCompatible));
  230. return latest;
  231. }
  232. export const isInstallControlsEnabled = () => config.pluginAdminEnabled;
  233. export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
  234. export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
  235. function isPluginVisible(id: string) {
  236. const { pluginCatalogHiddenPlugins }: { pluginCatalogHiddenPlugins: string[] } = config;
  237. return !pluginCatalogHiddenPlugins.includes(id);
  238. }
  239. export function isLocalCorePlugin(local?: LocalPlugin): boolean {
  240. return Boolean(local?.signature === 'internal');
  241. }