just do it

This commit is contained in:
Will Browne
2024-10-24 10:49:07 +01:00
parent 3523289e98
commit 367bfdddfe
13 changed files with 211 additions and 12 deletions
+1 -1
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;
+9
View File
@@ -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[] = [];
+8
View File
@@ -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"`
+22
View File
@@ -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
}
@@ -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'}
</Button>
)}
<Button variant="destructive" disabled={disableUninstall} onClick={onUninstall} title={uninstallTitle}>
<Button variant="destructive" disabled={disableUninstall} onClick={showConfirmModal} title={uninstallTitle}>
{uninstallBtnText}
</Button>
</Stack>
@@ -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 (
<Stack direction="column" gap={3} shrink={0} grow={0} maxWidth={'250px'}>
<Box padding={2} borderColor="medium" borderStyle="solid">
@@ -35,6 +57,56 @@ export function PluginDetailsRightPanel(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}>
<Text color="secondary">
<Trans i18nKey={'plugins.details.labels.pluginDependencies'}>Plugins: </Trans>
</Text>
<Stack direction="column" gap={2}>
{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>
</Stack>
)}
{config.pluginDependants && config.pluginDependants[plugin.id] && (
<Stack direction="column" gap={1}>
<Text color="secondary">
<Trans i18nKey={'plugins.details.labels.pluginDependants'}>Required by: </Trans>
</Text>
{config.pluginDependants[plugin.id].map((p) => {
return (
<TextLink key={p.pluginId} href={'/plugins/' + p.pluginId}>
<Icon name={PluginIconName[p.pluginType]} className={styles.icon} />
{p.pluginName} {p.pluginVersion}
</TextLink>
);
})}
</Stack>
)}
</Stack>
</Box>
)}
{plugin?.details?.links && plugin.details?.links?.length > 0 && (
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column" gap={2}>
@@ -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: [],
},
});
});
+32 -1
View File
@@ -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;
@@ -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: <PluginDetailsHeaderDependencies plugin={plugin} grafanaDependency={grafanaDependency} />,
+3 -4
View File
@@ -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;
+3
View File
@@ -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",
+4 -1
View File
@@ -2148,6 +2148,9 @@
"name-header": "",
"update-header": "",
"update-status-text": ""
},
"uninstall": {
"confirmation": ""
}
},
"details": {
@@ -2957,4 +2960,4 @@
"title": ""
}
}
}
}
@@ -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": "Ŝŧäŧūş",