From 367bfdddfe9014dddca74d817e0abb1db02e9970 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 24 Oct 2024 10:49:07 +0100 Subject: [PATCH] just do it --- packages/grafana-data/src/types/plugin.ts | 2 +- packages/grafana-runtime/src/config.ts | 9 +++ pkg/api/dtos/frontend_settings.go | 8 ++ pkg/api/frontendsettings.go | 22 ++++++ .../InstallControls/InstallControlsButton.tsx | 11 ++- .../components/PluginDetailsRightPanel.tsx | 76 ++++++++++++++++++- .../features/plugins/admin/helpers.test.ts | 41 ++++++++++ public/app/features/plugins/admin/helpers.ts | 33 +++++++- .../plugins/admin/hooks/usePluginInfo.tsx | 3 +- public/app/features/plugins/admin/types.ts | 7 +- public/locales/en-US/grafana.json | 3 + public/locales/es-ES/grafana.json | 5 +- public/locales/pseudo-LOCALE/grafana.json | 3 + 13 files changed, 211 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 1f64879d317..f7dd404a02b 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -102,7 +102,7 @@ export interface PluginMeta { moduleHash?: string; } -interface PluginDependencyInfo { +export interface PluginDependencyInfo { id: string; name: string; version: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 299db15ec35..89422b533f1 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -20,6 +20,7 @@ import { PluginLoadingStrategy, PluginDependencies, PluginExtensions, + PluginType, } from '@grafana/data'; export interface AzureSettings { @@ -54,6 +55,13 @@ export type PreinstalledPlugin = { version: string; }; +export type DependantInfo = { + pluginId: string; + pluginName: string; + pluginVersion: string; + pluginType: PluginType; +}; + export class GrafanaBootConfig implements GrafanaConfig { publicDashboardAccessToken?: string; publicDashboardsEnabled = true; @@ -139,6 +147,7 @@ export class GrafanaBootConfig implements GrafanaConfig { pluginCatalogManagedPlugins: string[] = []; pluginCatalogPreinstalledPlugins: PreinstalledPlugin[] = []; pluginsCDNBaseURL = ''; + pluginDependants?: { [key: string]: DependantInfo[] } = {}; expressionsEnabled = false; customTheme?: undefined; awsAllowedAuthProviders: string[] = []; diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index c0900e8ab47..15b62c30b2d 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -151,6 +151,13 @@ type FrontendSettingsSqlConnectionLimitsDTO struct { ConnMaxLifetime int `json:"connMaxLifetime"` } +type DependencyInfo struct { + PluginID string `json:"pluginId"` + PluginName string `json:"pluginName"` + PluginType string `json:"pluginType"` + PluginVersion string `json:"pluginVersion"` +} + type FrontendSettingsDTO struct { DefaultDatasource string `json:"defaultDatasource"` Datasources map[string]plugins.DataSourceDTO `json:"datasources"` @@ -236,6 +243,7 @@ type FrontendSettingsDTO struct { SnapshotEnabled bool `json:"snapshotEnabled"` SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"` ReportingStaticContext map[string]string `json:"reportingStaticContext"` + PluginDependencies map[string][]DependencyInfo `json:"pluginDependants"` Azure FrontendSettingsAzureDTO `json:"azure"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 2f09a859356..b38840209c2 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -242,6 +242,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable, ReportingStaticContext: hs.Cfg.ReportingStaticContext, ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset, + PluginDependencies: pluginDependencyMap(c.Req.Context(), hs.pluginStore), BuildInfo: dtos.FrontendSettingsBuildInfoDTO{ HideVersion: hideVersion, @@ -765,3 +766,24 @@ func (hs *HTTPServer) getEnabledOAuthProviders() map[string]any { } return providers } + +// pluginDependencyMap returns a map of dependant plugin IDs to their parent. +func pluginDependencyMap(ctx context.Context, pluginStore pluginstore.Store) map[string][]dtos.DependencyInfo { + dependencies := make(map[string][]dtos.DependencyInfo) + + for _, plugin := range pluginStore.Plugins(ctx) { + for _, dep := range plugin.Dependencies.Plugins { + if _, exists := dependencies[dep.ID]; !exists { + dependencies[dep.ID] = []dtos.DependencyInfo{} + } + dependencies[dep.ID] = append(dependencies[dep.ID], dtos.DependencyInfo{ + PluginID: plugin.ID, + PluginVersion: plugin.Info.Version, + PluginName: plugin.Name, + PluginType: string(plugin.Type), + }) + } + } + + return dependencies +} diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index fa82e9ce628..4a82a4948fe 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -125,6 +125,13 @@ export function InstallControlsButton({ uninstallTitle = 'Preinstalled plugin. Remove from Grafana config before uninstalling.'; } + // TODO && parent plugin is still installed + const dependencyOf = plugin.details?.dependantPlugins?.map((dep) => dep.pluginName); + if (dependencyOf?.length) { + disableUninstall = true; + uninstallTitle = `Dependent plugins must be removed first: ${dependencyOf.join(', ')}`; + } + if (pluginStatus === PluginStatus.UNINSTALL) { return ( <> @@ -147,7 +154,7 @@ export function InstallControlsButton({ } if (!plugin.isPublished || hasInstallWarning) { - // Cannot be updated or installed + // Cannot be updated/installed/uninstalled return null; } @@ -164,7 +171,7 @@ export function InstallControlsButton({ {isInstalling ? 'Updating' : 'Update'} )} - diff --git a/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx index 86d6d6650d8..479216c01bc 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx @@ -1,9 +1,15 @@ +import * as React from 'react'; + +import { config } from '@grafana/runtime'; import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage'; -import { Stack, Text, LinkButton, Box, TextLink } from '@grafana/ui'; +import { Stack, Text, LinkButton, Box, TextLink, Icon, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { formatDate } from 'app/core/internationalization/dates'; -import { CatalogPlugin } from '../types'; +import { getLatestCompatibleVersion } from '../helpers'; +import { CatalogPlugin, PluginIconName } from '../types'; + +import { getStyles } from './PluginDetailsHeaderDependencies'; type Props = { info: PageInfoItem[]; @@ -12,6 +18,22 @@ type Props = { export function PluginDetailsRightPanel(props: Props): React.ReactElement | null { const { info, plugin } = props; + const styles = useStyles2(getStyles); + + const pluginDependencies = plugin.details?.pluginDependencies; + let grafanaDependency = plugin.details?.grafanaDependency; + const useLatestCompatibleInfo = !plugin.isInstalled; + const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions); + if (useLatestCompatibleInfo && latestCompatibleVersion?.grafanaDependency) { + grafanaDependency = latestCompatibleVersion?.grafanaDependency; + } + + if (!grafanaDependency) { + grafanaDependency = 'unknown'; + } + + const hasDependencyInfo = grafanaDependency || (pluginDependencies && pluginDependencies.length); + return ( @@ -35,6 +57,56 @@ export function PluginDetailsRightPanel(props: Props): React.ReactElement | null + {hasDependencyInfo && ( + + + + Dependencies + + + + + Grafana {grafanaDependency} + + + + {pluginDependencies && pluginDependencies.length > 0 && ( + + + Plugins: + + + {pluginDependencies.map((p) => { + return ( + + + {p.name} {p.version} + + ); + })} + + + )} + + {config.pluginDependants && config.pluginDependants[plugin.id] && ( + + + Required by: + + {config.pluginDependants[plugin.id].map((p) => { + return ( + + + {p.pluginName} {p.pluginVersion} + + ); + })} + + )} + + + )} + {plugin?.details?.links && plugin.details?.links?.length > 0 && ( diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index 95152175022..5cfece99093 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -215,6 +215,11 @@ describe('Plugins/Helpers', () => { updatedAt: '2021-05-18T14:53:01.000Z', isFullyInstalled: false, angularDetected: false, + details: { + dependantPlugins: [], + links: [], + pluginDependencies: [], + }, }); }); @@ -297,6 +302,24 @@ describe('Plugins/Helpers', () => { installedVersion: '4.2.2', isFullyInstalled: true, angularDetected: false, + details: { + dependantPlugins: [], + links: [ + { + name: 'GitHub', + url: 'https://github.com/alexanderzobnin/grafana-zabbix', + }, + { + name: 'Docs', + url: 'https://alexanderzobnin.github.io/grafana-zabbix', + }, + { + name: 'License', + url: 'https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE', + }, + ], + pluginDependencies: [], + }, }); }); @@ -352,6 +375,24 @@ describe('Plugins/Helpers', () => { installedVersion: '4.2.2', isFullyInstalled: true, angularDetected: false, + details: { + dependantPlugins: [], + links: [ + { + name: 'GitHub', + url: 'https://github.com/alexanderzobnin/grafana-zabbix', + }, + { + name: 'Docs', + url: 'https://alexanderzobnin.github.io/grafana-zabbix', + }, + { + name: 'License', + url: 'https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE', + }, + ], + pluginDependencies: [], + }, }); }); diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index b68a6dd44ee..f01ab5e4857 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -1,7 +1,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data'; -import { config, featureEnabled } from '@grafana/runtime'; +import { config, DependantInfo, featureEnabled } from '@grafana/runtime'; import configCore, { Settings } from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; @@ -153,6 +153,11 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C angularDetected, isFullyInstalled: isDisabled, latestVersion: plugin.version, + details: { + pluginDependencies: plugin.json?.dependencies?.plugins || [], + dependantPlugins: dependantPlugins(id), + links: plugin.json?.info.links || [], + }, }; } @@ -203,6 +208,11 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isFullyInstalled: true, iam: plugin.iam, latestVersion: plugin.latestVersion, + details: { + pluginDependencies: plugin.dependencies?.plugins || [], + dependantPlugins: dependantPlugins(id), + links: plugin.info.links || [], + }, }; } @@ -266,6 +276,11 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isFullyInstalled: Boolean(local) || isDisabled, iam: local?.iam, latestVersion: local?.latestVersion || remote?.version || '', + details: { + pluginDependencies: local?.dependencies?.plugins || remote?.json?.dependencies?.plugins || [], + dependantPlugins: dependantPlugins(id), + links: local?.info.links || remote?.json?.info.links || [], + }, }; } @@ -388,6 +403,22 @@ export function isManagedPlugin(id: string) { return pluginCatalogManagedPlugins?.includes(id); } +export function dependantPlugins(id: string): DependantInfo[] { + const { pluginDependants } = config; + if (!pluginDependants) { + return []; + } + + const dependants: DependantInfo[] = []; + if (pluginDependants[id]) { + for (let dependant of pluginDependants[id]) { + dependants.push(dependant); + } + } + + return dependants; +} + export function isPreinstalledPlugin(id: string): { found: boolean; withVersion: boolean } { const { pluginCatalogPreinstalledPlugins } = config; diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index 0b4fca761a5..b00a70da041 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, PluginSignatureType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import { PageInfoItem } from '../../../../core/components/Page/types'; @@ -64,7 +65,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { } const hasNoDependencyInfo = !grafanaDependency && (!pluginDependencies || !pluginDependencies.length); - if (!hasNoDependencyInfo) { + if (!hasNoDependencyInfo && !config.featureToggles.pluginsDetailsRightPanel) { info.push({ label: t('plugins.details.labels.dependencies', 'Dependencies'), value: , diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 4f0796826ff..a95c476870b 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -8,6 +8,7 @@ import { PluginErrorCode, WithAccessControlMetadata, } from '@grafana/data'; +import { DependantInfo } from '@grafana/runtime'; import { IconName } from '@grafana/ui'; import { StoreState, PluginsState } from 'app/types'; @@ -69,12 +70,10 @@ export interface CatalogPlugin extends WithAccessControlMetadata { export interface CatalogPluginDetails { readme?: string; versions?: Version[]; - links: Array<{ - name: string; - url: string; - }>; + links: Rel[]; grafanaDependency?: string; pluginDependencies?: PluginDependencies['plugins']; + dependantPlugins?: DependantInfo[]; statusContext?: string; iam?: IdentityAccessManagement; changelog?: string; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 46c91cab42a..8d714121cd5 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2156,7 +2156,10 @@ "dependencies": "Dependencies", "downloads": "Downloads", "from": "From", + "grafanaDependency": "Grafana ", "links": "Links ", + "pluginDependants": "Required by: ", + "pluginDependencies": "Plugins: ", "reportAbuse": "Report a concern ", "signature": "Signature", "status": "Status", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index f54acf9155f..b3edaa1ae49 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -2148,6 +2148,9 @@ "name-header": "", "update-header": "", "update-status-text": "" + }, + "uninstall": { + "confirmation": "" } }, "details": { @@ -2957,4 +2960,4 @@ "title": "" } } -} \ No newline at end of file +} diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index df37041278f..49ecf9e1bf2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2156,7 +2156,10 @@ "dependencies": "Đępęʼnđęʼnčįęş", "downloads": "Đőŵʼnľőäđş", "from": "Fřőm", + "grafanaDependency": "Ğřäƒäʼnä ", "links": "Ŀįʼnĸş ", + "pluginDependants": "Ŗęqūįřęđ þy: ", + "pluginDependencies": "Pľūģįʼnş: ", "reportAbuse": "Ŗępőřŧ ä čőʼnčęřʼn ", "signature": "Ŝįģʼnäŧūřę", "status": "Ŝŧäŧūş",