just do it
This commit is contained in:
@@ -102,7 +102,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
||||
moduleHash?: string;
|
||||
}
|
||||
|
||||
interface PluginDependencyInfo {
|
||||
export interface PluginDependencyInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+9
-2
@@ -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: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Ŝŧäŧūş",
|
||||
|
||||
Reference in New Issue
Block a user