Compare commits

...

28 Commits

Author SHA1 Message Date
Will Browne
335f005dac update 2025-03-24 12:03:18 +00:00
Will Browne
da01063688 allow uninstall for dependency plugins 2025-03-24 11:25:33 +00:00
Will Browne
bc498dc780 Merge branch 'main' into plugin-dependency-install 2025-03-24 11:19:16 +00:00
Will Browne
5108ca2ac8 Merge branch 'main' into plugin-dependency-install 2025-03-24 10:43:01 +00:00
Will Browne
c01fb907ad merge with main 2025-03-20 13:29:46 +00:00
Will Browne
440587a510 Merge branch 'main' into plugin-dependency-install 2025-03-19 14:33:06 +00:00
Will Browne
949025bbf3 Merge branch 'main' into plugin-dependency-install 2025-03-19 14:22:49 +00:00
Will Browne
f33ac76119 merge with main 2025-03-19 13:55:16 +00:00
Will Browne
e6d4dffe4f Merge branch 'main' into plugin-dependency-install 2025-03-14 15:35:37 +00:00
Will Browne
3228650d22 Merge branch 'main' into plugin-dependency-install 2025-03-04 14:33:55 +00:00
Will Browne
4011ac4dd2 Merge branch 'main' into plugin-dependency-install 2025-03-04 10:28:18 +00:00
Will Browne
3e67467872 remove dependant uninstall warning 2025-02-27 11:47:44 +00:00
Will Browne
6ad57d0e6e undo changes 2025-02-27 11:25:09 +00:00
Will Browne
a9c410c380 fix import 2025-02-27 11:21:32 +00:00
Will Browne
6b62cbb5b0 merge with main 2025-02-27 10:58:29 +00:00
Will Browne
374fede490 add missing modal 2025-02-25 13:48:47 +00:00
Will Browne
2f87e10296 merge with main 2025-02-25 12:10:49 +00:00
Will Browne
e547ee52b5 fix linter + tests 2025-02-25 12:10:06 +00:00
Will Browne
81110f0ea3 fix dependant plugins setting 2025-02-24 17:05:01 +00:00
Will Browne
0c01993ed4 merge with main 2025-02-24 15:38:58 +00:00
Will Browne
00e6b0ea9f remove duplicate types 2025-02-24 15:25:22 +00:00
Will Browne
919dc5377c simplify 2025-02-24 15:05:46 +00:00
Will Browne
6ab5581fc5 merge with main 2025-02-21 17:49:24 +00:00
Will Browne
c9ff568c91 fix show when empty 2024-10-25 11:58:19 +01:00
Will Browne
30696c0966 tidy 2024-10-24 11:12:33 +01:00
Will Browne
705c86b6fe remove comma 2024-10-24 10:53:40 +01:00
Will Browne
34f4409b0d undo i18n changes 2024-10-24 10:52:41 +01:00
Will Browne
367bfdddfe just do it 2024-10-24 10:49:07 +01:00
16 changed files with 179 additions and 40 deletions

View File

@@ -4862,7 +4862,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/plugins/admin/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],

View File

@@ -981,6 +981,7 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8=
github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=

View File

@@ -594,6 +594,7 @@ export {
type AngularMeta,
type PluginMeta,
type PluginDependencies,
type PluginDependencyInfo,
type PluginExtensions,
type PluginInclude,
type PluginBuildInfo,

View File

@@ -102,7 +102,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
moduleHash?: string;
}
interface PluginDependencyInfo {
export interface PluginDependencyInfo {
id: string;
name: string;
version: string;

View File

@@ -19,6 +19,7 @@ import {
AngularMeta,
PluginLoadingStrategy,
PluginDependencies,
PluginDependencyInfo,
PluginExtensions,
} from '@grafana/data';
@@ -140,6 +141,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
pluginCatalogManagedPlugins: string[] = [];
pluginCatalogPreinstalledPlugins: PreinstalledPlugin[] = [];
pluginsCDNBaseURL = '';
pluginDependants?: { [key: string]: PluginDependencyInfo[] } = {};
expressionsEnabled = false;
awsAllowedAuthProviders: string[] = [];
awsAssumeRoleEnabled = false;

View File

@@ -2,6 +2,7 @@ package dtos
import (
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
@@ -244,6 +245,7 @@ type FrontendSettingsDTO struct {
SnapshotEnabled bool `json:"snapshotEnabled"`
SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"`
ReportingStaticContext map[string]string `json:"reportingStaticContext"`
PluginDependencies map[string][]plugins.Dependency `json:"pluginDependants"`
Azure FrontendSettingsAzureDTO `json:"azure"`

View File

@@ -248,6 +248,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
ExploreHideLogsDownload: hs.Cfg.ExploreHideLogsDownload,
DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle,
PluginDependencies: pluginDependencyMap(c.Req.Context(), hs.pluginStore),
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{
HideVersion: hideVersion,
@@ -790,3 +791,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][]plugins.Dependency {
dependencies := make(map[string][]plugins.Dependency)
for _, plugin := range pluginStore.Plugins(ctx) {
for _, dep := range plugin.Dependencies.Plugins {
if _, exists := dependencies[dep.ID]; !exists {
dependencies[dep.ID] = []plugins.Dependency{}
}
dependencies[dep.ID] = append(dependencies[dep.ID], plugins.Dependency{
ID: plugin.ID,
Version: plugin.Info.Version,
Name: plugin.Name,
Type: string(plugin.Type),
})
}
}
return dependencies
}

View File

@@ -21,6 +21,7 @@ require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect

View File

@@ -26,8 +26,8 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=

View File

@@ -130,12 +130,19 @@ export function InstallControlsButton({
uninstallTitle = 'Preinstalled plugin. Remove from Grafana config before uninstalling.';
}
let uninstallConfirmationBody = 'Are you sure you want to uninstall this plugin?';
// TODO && dependant plugin is still installed
const dependencyOf = plugin.dependantPlugins?.map((dep) => dep.name);
if (dependencyOf?.length) {
uninstallConfirmationBody = `This plugin is a dependency of ${dependencyOf.join(', ')}. Are you sure you want to uninstall this plugin?`;
}
const uninstallControls = (
<>
<ConfirmModal
isOpen={isConfirmModalVisible}
title={`Uninstall ${plugin.name}`}
body="Are you sure you want to uninstall this plugin?"
body={uninstallConfirmationBody}
confirmText="Confirm"
icon="exclamation-triangle"
onConfirm={onUninstall}
@@ -162,7 +169,7 @@ export function InstallControlsButton({
}
if (!plugin.isPublished || hasInstallWarning) {
// Cannot be updated or installed
// Cannot be updated/installed/uninstalled
return null;
}

View File

@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { GrafanaTheme2, PluginDependencyInfo } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage';
import {
Stack,
@@ -20,7 +20,8 @@ import {
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';
type Props = {
pluginExtentionsInfo: PageInfoItem[];
@@ -51,6 +52,25 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
});
};
const pluginDependencies = plugin.details?.pluginDependencies;
let grafanaDependency = plugin.details?.grafanaDependency;
if (!grafanaDependency) {
grafanaDependency = 'unknown';
}
const useLatestCompatibleInfo = !plugin.isInstalled;
const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions);
if (useLatestCompatibleInfo && latestCompatibleVersion?.grafanaDependency) {
grafanaDependency = latestCompatibleVersion?.grafanaDependency;
}
let pluginDependants: PluginDependencyInfo[] = [];
if (config.pluginDependants && config.pluginDependants[plugin.id]) {
pluginDependants = config.pluginDependants[plugin.id];
}
const hasDependencyInfo =
grafanaDependency || (pluginDependencies && pluginDependencies.length) || pluginDependants.length;
return (
<>
<Stack direction="column" gap={3} shrink={0} grow={0} width={width} data-testid="plugin-details-panel">
@@ -90,6 +110,50 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
)}
</Stack>
</Box>
{hasDependencyInfo && (
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column" gap={1}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.dependencies">Dependencies</Trans>
</Text>
<Stack direction="column" gap={1}>
<span className={styles.depBadge}>
<Icon name="grafana" className={styles.icon} />
<Trans i18nKey="plugins.details.labels.grafanaDependency">Grafana </Trans> {grafanaDependency}
</span>
</Stack>
{pluginDependencies && pluginDependencies.length > 0 && (
<Stack direction="column" gap={1}>
{pluginDependencies.map((p) => {
return (
<TextLink key={p.id} href={'/plugins/' + p.id}>
<Icon name={PluginIconName[p.type]} className={styles.icon} />
{p.name} {p.version}
</TextLink>
);
})}
</Stack>
)}
{pluginDependants && pluginDependants.length > 0 && (
<Stack direction="column" gap={1}>
<Text color="secondary">
<Trans i18nKey={'plugins.details.labels.pluginDependants'}>Required by: </Trans>
</Text>
{pluginDependants.map((p) => {
return (
<TextLink key={p.id} href={'/plugins/' + p.id}>
<Icon name={PluginIconName[p.type]} className={styles.icon} />
{p.name} {p.version}
</TextLink>
);
})}
</Stack>
)}
</Stack>
</Box>
)}
{shouldRenderLinks && (
<>
<Box padding={2} borderColor="medium" borderStyle="solid">
@@ -241,5 +305,13 @@ export const getStyles = (theme: GrafanaTheme2) => {
pluginVersionDetails: css({
wordBreak: 'break-word',
}),
depBadge: css({
display: 'flex',
alignItems: 'flex-start',
}),
icon: css({
color: theme.colors.text.secondary,
marginRight: theme.spacing(0.5),
}),
};
};

View File

@@ -349,6 +349,7 @@ describe('Plugins/Helpers', () => {
isFullyInstalled: true,
angularDetected: false,
url: 'https://github.com/alexanderzobnin/grafana-zabbix',
dependantPlugins: [],
});
});

View File

@@ -1,6 +1,13 @@
import uFuzzy from '@leeoniya/ufuzzy';
import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data';
import {
PluginSignatureStatus,
dateTimeParse,
PluginError,
PluginType,
PluginErrorCode,
PluginDependencyInfo,
} from '@grafana/data';
import { config, featureEnabled } from '@grafana/runtime';
import { Settings } from 'app/core/config';
import { contextSrv } from 'app/core/core';
@@ -62,11 +69,14 @@ export function mergeLocalsAndRemotes({
if (!shouldSkip) {
const catalogPlugin = mergeLocalAndRemote(localCounterpart, remotePlugin, error);
const isDependency = catalogPlugin?.dependantPlugins && catalogPlugin?.dependantPlugins.length > 0;
// 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;
: (instancesMap.has(remotePlugin.slug) || provisionedSet.has(remotePlugin.slug) || isDependency) &&
catalogPlugin.isInstalled;
catalogPlugin.isInstalled = instancesMap.has(remotePlugin.slug) || catalogPlugin.isInstalled;
@@ -80,7 +90,8 @@ export function mergeLocalsAndRemotes({
catalogPlugin.hasUpdate = true;
}
catalogPlugin.isUninstallingFromInstance = Boolean(localCounterpart) && !instancesMap.has(remotePlugin.slug);
catalogPlugin.isUninstallingFromInstance =
Boolean(localCounterpart) && !instancesMap.has(remotePlugin.slug) && !isDependency;
catalogPlugin.isProvisioned = provisionedSet.has(remotePlugin.slug);
}
@@ -274,6 +285,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
iam: local?.iam,
latestVersion: local?.latestVersion || remote?.version || '',
url: remote?.url || '',
dependantPlugins: dependantPlugins(id),
};
}
@@ -395,6 +407,22 @@ export function isManagedPlugin(id: string) {
return pluginCatalogManagedPlugins?.includes(id);
}
export function dependantPlugins(id: string): PluginDependencyInfo[] {
const { pluginDependants } = config;
if (!pluginDependants) {
return [];
}
const dependants: PluginDependencyInfo[] = [];
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;

View File

@@ -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';
@@ -84,7 +85,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: <PluginDetailsHeaderDependencies plugin={plugin} grafanaDependency={grafanaDependency} />,

View File

@@ -64,15 +64,13 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
iam?: IdentityAccessManagement;
isProvisioned?: boolean;
url?: string;
dependantPlugins?: any[];
}
export interface CatalogPluginDetails {
readme?: string;
versions?: Version[];
links: Array<{
name: string;
url: string;
}>;
links: Rel[];
grafanaDependency?: string;
pluginDependencies?: PluginDependencies['plugins'];
statusContext?: string;

View File

@@ -3574,10 +3574,12 @@
"documentation": "Documentation",
"downloads": "Downloads",
"from": "From",
"grafanaDependency": "Grafana ",
"installedVersion": "Installed Version",
"lastCommitDate": "Last commit date:",
"latestVersion": "Latest Version",
"license": "License",
"pluginDependants": "Required by: ",
"raiseAnIssue": "Raise an issue",
"reportAbuse": "Report a concern ",
"reportAbuseTooltip": "Report issues related to malicious or harmful plugins directly to Grafana Labs.",