Files
Hugo Häggmark 20c700dd52 Chore: reduces barrel files part II (#107688)
* Chore: reduce barrel files

* chore: fixes unit test

* Chore: reduces barrel files part II

* chore: fix import sorting
2025-07-09 06:15:33 +02:00

479 lines
15 KiB
TypeScript

import uFuzzy from '@leeoniya/ufuzzy';
import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data';
import { config, featureEnabled } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types/accessControl';
import {
CatalogPlugin,
InstancePlugin,
LocalPlugin,
ProvisionedPlugin,
RemotePlugin,
RemotePluginStatus,
Version,
} from './types';
export function mergeLocalsAndRemotes({
local = [],
remote = [],
instance = [],
provisioned = [],
pluginErrors: errors,
}: {
local: LocalPlugin[];
remote?: RemotePlugin[];
instance?: InstancePlugin[];
provisioned?: ProvisionedPlugin[];
pluginErrors?: PluginError[];
}): CatalogPlugin[] {
const catalogPlugins: CatalogPlugin[] = [];
const errorByPluginId = groupErrorsByPluginId(errors);
const instancesMap = instance.reduce((map, instancePlugin) => {
map.set(instancePlugin.pluginSlug, instancePlugin);
return map;
}, new Map<string, InstancePlugin>());
const provisionedSet = provisioned.reduce((map, provisionedPlugin) => {
map.add(provisionedPlugin.slug);
return map;
}, new Set<string>());
// add locals
local.forEach((localPlugin) => {
const remoteCounterpart = remote.find((r) => r.slug === localPlugin.id);
const error = errorByPluginId[localPlugin.id];
if (!remoteCounterpart) {
catalogPlugins.push(mergeLocalAndRemote(localPlugin, undefined, error));
}
});
// add remote
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.
if (!shouldSkip) {
const catalogPlugin = mergeLocalAndRemote(localCounterpart, remotePlugin, error);
// for managed instances, check if plugin is installed, but not yet present in the current instance
if (config.pluginAdminExternalManageEnabled) {
catalogPlugin.isFullyInstalled = catalogPlugin.isCore
? true
: (instancesMap.has(remotePlugin.slug) || provisionedSet.has(remotePlugin.slug)) && catalogPlugin.isInstalled;
catalogPlugin.isInstalled = instancesMap.has(remotePlugin.slug) || catalogPlugin.isInstalled;
const instancePlugin = instancesMap.get(remotePlugin.slug);
catalogPlugin.isUpdatingFromInstance =
instancesMap.has(remotePlugin.slug) &&
catalogPlugin.hasUpdate &&
catalogPlugin.installedVersion !== instancePlugin?.version;
if (instancePlugin?.version && instancePlugin?.version !== remotePlugin.version) {
catalogPlugin.hasUpdate = true;
}
catalogPlugin.isUninstallingFromInstance = Boolean(localCounterpart) && !instancesMap.has(remotePlugin.slug);
catalogPlugin.isProvisioned = provisionedSet.has(remotePlugin.slug);
}
catalogPlugins.push(catalogPlugin);
}
});
return catalogPlugins;
}
export function mergeLocalAndRemote(local?: LocalPlugin, remote?: RemotePlugin, error?: PluginError): CatalogPlugin {
if (!local && remote) {
return mapRemoteToCatalog(remote, error);
}
if (local && !remote) {
return mapLocalToCatalog(local, error);
}
return mapToCatalogPlugin(local, remote, error);
}
export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): CatalogPlugin {
const {
name,
slug: id,
description,
version,
orgName,
popularity,
downloads,
typeCode,
updatedAt,
createdAt: publishedAt,
status,
angularDetected,
keywords,
signatureType,
versionSignatureType,
versionSignedByOrgName,
url,
} = plugin;
const isDisabled = !!error;
return {
description,
downloads,
id,
info: {
logos: {
small: `${config.appSubUrl}/api/gnet/plugins/${id}/versions/${version}/logos/small`,
large: `${config.appSubUrl}/api/gnet/plugins/${id}/versions/${version}/logos/large`,
},
keywords,
},
name,
orgName,
popularity,
publishedAt,
signature: getPluginSignature({ remote: plugin, error }),
signatureType: signatureType || versionSignatureType || undefined,
signatureOrg: versionSignedByOrgName,
updatedAt,
hasUpdate: false,
isPublished: true,
isInstalled: isDisabled,
isDisabled: isDisabled,
isManaged: isManagedPlugin(id),
isPreinstalled: isPreinstalledPlugin(id),
isDeprecated: status === RemotePluginStatus.Deprecated,
isCore: plugin.internal,
isDev: false,
isEnterprise: status === RemotePluginStatus.Enterprise,
type: typeCode,
error: error?.errorCode,
angularDetected,
isFullyInstalled: isDisabled,
latestVersion: plugin.version,
url,
};
}
export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): CatalogPlugin {
const {
name,
info: { description, version, logos, updated, author, keywords },
id,
dev,
type,
signature,
signatureOrg,
signatureType,
hasUpdate,
accessControl,
angularDetected,
} = plugin;
const isDisabled = !!error;
return {
description,
downloads: 0,
id,
info: { logos, keywords },
name,
orgName: author.name,
popularity: 0,
publishedAt: '',
signature: getPluginSignature({ local: plugin, error }),
signatureOrg,
signatureType,
updatedAt: updated,
installedVersion: version,
hasUpdate,
isInstalled: true,
isDisabled: isDisabled,
isCore: signature === 'internal',
isPublished: false,
isDeprecated: false,
isDev: Boolean(dev),
isEnterprise: false,
isManaged: isManagedPlugin(id),
isPreinstalled: isPreinstalledPlugin(id),
type,
error: error?.errorCode,
accessControl: accessControl,
angularDetected,
isFullyInstalled: true,
iam: plugin.iam,
latestVersion: plugin.latestVersion,
};
}
// TODO: change the signature by removing the optionals for local and remote.
export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, error?: PluginError): CatalogPlugin {
const installedVersion = local?.info.version;
const id = remote?.slug || local?.id || '';
const type = local?.type || remote?.typeCode;
const isDisabled = !!error;
const keywords = remote?.keywords || local?.info.keywords || [];
let logos = {
small: `/public/build/img/icn-${type}.svg`,
large: `/public/build/img/icn-${type}.svg`,
};
if (remote) {
logos = {
small: `${config.appSubUrl}/api/gnet/plugins/${id}/versions/${remote.version}/logos/small`,
large: `${config.appSubUrl}/api/gnet/plugins/${id}/versions/${remote.version}/logos/large`,
};
} else if (local && local.info.logos) {
logos = local.info.logos;
}
return {
description: local?.info.description || remote?.description || '',
downloads: remote?.downloads || 0,
hasUpdate: local?.hasUpdate || false,
id,
info: {
logos,
keywords,
},
isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
isDev: Boolean(local?.dev),
isEnterprise: remote?.status === RemotePluginStatus.Enterprise,
isInstalled: Boolean(local) || isDisabled,
isDisabled: isDisabled,
isDeprecated: remote?.status === RemotePluginStatus.Deprecated,
isPublished: true,
isManaged: isManagedPlugin(id),
isPreinstalled: isPreinstalledPlugin(id),
// TODO<check if we would like to keep preferring the remote version>
name: remote?.name || local?.name || '',
// TODO<check if we would like to keep preferring the remote version>
orgName: remote?.orgName || local?.info.author.name || '',
popularity: remote?.popularity || 0,
publishedAt: remote?.createdAt || '',
type,
signature: getPluginSignature({ local, remote, error }),
signatureOrg: local?.signatureOrg || remote?.versionSignedByOrgName,
signatureType: local?.signatureType || remote?.versionSignatureType || remote?.signatureType || undefined,
// TODO<check if we would like to keep preferring the remote version>
updatedAt: remote?.updatedAt || local?.info.updated || '',
installedVersion,
error: error?.errorCode,
// Only local plugins have access control metadata
accessControl: local?.accessControl,
angularDetected: local?.angularDetected ?? remote?.angularDetected,
isFullyInstalled: Boolean(local) || isDisabled,
iam: local?.iam,
latestVersion: local?.latestVersion || remote?.version || '',
url: remote?.url || '',
};
}
export const getExternalManageLink = (pluginId: string) => `${config.pluginCatalogURL}${pluginId}`;
export enum Sorters {
nameAsc = 'nameAsc',
nameDesc = 'nameDesc',
updated = 'updated',
published = 'published',
downloads = 'downloads',
}
export const sortPlugins = (plugins: CatalogPlugin[], sortBy: Sorters) => {
const sorters: { [name: string]: (a: CatalogPlugin, b: CatalogPlugin) => number } = {
nameAsc: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name),
nameDesc: (a: CatalogPlugin, b: CatalogPlugin) => b.name.localeCompare(a.name),
updated: (a: CatalogPlugin, b: CatalogPlugin) =>
dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(),
published: (a: CatalogPlugin, b: CatalogPlugin) =>
dateTimeParse(b.publishedAt).valueOf() - dateTimeParse(a.publishedAt).valueOf(),
downloads: (a: CatalogPlugin, b: CatalogPlugin) => b.downloads - a.downloads,
};
if (sorters[sortBy]) {
return plugins.sort(sorters[sortBy]);
}
return plugins;
};
function groupErrorsByPluginId(errors: PluginError[] = []): Record<string, PluginError | undefined> {
return errors.reduce<Record<string, PluginError | undefined>>((byId, error) => {
byId[error.pluginId] = error;
return byId;
}, {});
}
function getPluginSignature(options: {
local?: LocalPlugin;
remote?: RemotePlugin;
error?: PluginError;
}): PluginSignatureStatus {
const { error, local, remote } = options;
if (error) {
switch (error.errorCode) {
case PluginErrorCode.invalidSignature:
return PluginSignatureStatus.invalid;
case PluginErrorCode.missingSignature:
return PluginSignatureStatus.missing;
case PluginErrorCode.modifiedSignature:
return PluginSignatureStatus.modified;
}
}
if (local?.signature) {
return local.signature;
}
if (remote?.signatureType && remote?.versionSignatureType) {
return PluginSignatureStatus.valid;
}
return PluginSignatureStatus.missing;
}
export function getLatestCompatibleVersion(versions: Version[] | undefined): Version | undefined {
if (!versions) {
return;
}
const [latest] = versions.filter((v) => Boolean(v.isCompatible));
return latest;
}
export const isInstallControlsEnabled = () => config.pluginAdminEnabled;
export const hasInstallControlWarning = (
plugin: CatalogPlugin,
isRemotePluginsAvailable: boolean,
latestCompatibleVersion?: Version
) => {
const isExternallyManaged = config.pluginAdminExternalManageEnabled;
const hasPermission = contextSrv.hasPermission(AccessControlAction.PluginsInstall);
const isCompatible = Boolean(latestCompatibleVersion);
return (
plugin.type === PluginType.renderer ||
(plugin.isEnterprise && !featureEnabled('enterprise.plugins')) ||
plugin.isDev ||
(!hasPermission && !isExternallyManaged) ||
!plugin.isPublished ||
!isCompatible ||
!isRemotePluginsAvailable
);
};
export const isLocalPluginVisibleByConfig = (p: LocalPlugin) => isNotHiddenByConfig(p.id);
export const isRemotePluginVisibleByConfig = (p: RemotePlugin) => isNotHiddenByConfig(p.slug);
function isNotHiddenByConfig(id: string) {
const { pluginCatalogHiddenPlugins }: { pluginCatalogHiddenPlugins: string[] } = config;
return !pluginCatalogHiddenPlugins.includes(id);
}
export function isManagedPlugin(id: string) {
const { pluginCatalogManagedPlugins }: { pluginCatalogManagedPlugins: string[] } = config;
return pluginCatalogManagedPlugins?.includes(id);
}
export function isPreinstalledPlugin(id: string): { found: boolean; withVersion: boolean } {
const { pluginCatalogPreinstalledPlugins } = config;
const plugin = pluginCatalogPreinstalledPlugins?.find((p) => p.id === id);
return { found: !!plugin?.id, withVersion: !!plugin?.version };
}
export function isLocalCorePlugin(local?: LocalPlugin): boolean {
return Boolean(local?.signature === 'internal');
}
function getId(inputString: string): string {
const parts = inputString.split(' - ');
return parts[0];
}
function getPluginDetailsForFuzzySearch(plugins: CatalogPlugin[]): string[] {
return plugins.reduce((result: string[], { id, name, type, orgName, info }: CatalogPlugin) => {
const keywordsForSearch = info.keywords?.join(' ').toLowerCase();
const pluginString = `${id} - ${name} - ${type} - ${orgName} - ${keywordsForSearch}`;
result.push(pluginString);
return result;
}, []);
}
export function filterByKeyword(plugins: CatalogPlugin[], query: string) {
const dataArray = getPluginDetailsForFuzzySearch(plugins);
let uf = new uFuzzy({ intraMode: 1, intraSub: 0 });
let idxs = uf.filter(dataArray, query);
if (idxs === null) {
return null;
}
return idxs.map((id) => getId(dataArray[id]));
}
function isPluginModifiable(plugin: CatalogPlugin) {
if (
plugin.isProvisioned || //provisioned plugins cannot be modified
plugin.isCore || //core plugins cannot be modified
plugin.type === PluginType.renderer || // currently renderer plugins are not supported by the catalog due to complications related to installation / update / uninstall
plugin.isPreinstalled.withVersion || // Preinstalled plugins (with specified version) cannot be modified
plugin.isManaged // Managed plugins cannot be modified
) {
return false;
}
return true;
}
export function isPluginUpdatable(plugin: CatalogPlugin) {
if (!isPluginModifiable(plugin)) {
return false;
}
// If there is no update available, the plugin cannot be updated
if (!plugin.hasUpdate) {
return false;
}
// If the plugin is currently being updated, it should not be updated
if (plugin.isUpdatingFromInstance) {
return false;
}
return true;
}
export function shouldDisablePluginInstall(plugin: CatalogPlugin) {
if (
!isPluginModifiable(plugin) ||
(plugin.isEnterprise && !featureEnabled('enterprise.plugins')) ||
!plugin.isPublished ||
plugin.isDisabled ||
!isInstallControlsEnabled()
) {
return true;
}
return false;
}
export function isNonAngularVersion(version?: Version) {
if (!version) {
return false;
}
return version.angularDetected === false;
}
export function isDisabledAngularPlugin(plugin: CatalogPlugin) {
return plugin.isDisabled && plugin.error === PluginErrorCode.angular;
}