From 98778289cbf66d794b3f05f87f177bba5ff52322 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 19 Apr 2023 10:22:24 +0200 Subject: [PATCH] Catalog: Show install error with incompatible version (#65059) --- pkg/api/plugins.go | 4 ++++ pkg/plugins/repo/models.go | 9 +++++++++ pkg/plugins/repo/service.go | 7 +++---- public/app/features/plugins/admin/api.ts | 5 ++++- .../InstallControls/InstallControlsButton.tsx | 19 ++++++++++++++----- .../admin/components/PluginSubtitle.tsx | 10 ++++++++-- .../features/plugins/admin/state/actions.ts | 5 +++++ .../app/features/plugins/admin/state/hooks.ts | 8 +++++++- 8 files changed, 54 insertions(+), 13 deletions(-) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index b5313141a2c..c628009d458 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -469,6 +469,10 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons if errors.Is(err, plugins.ErrInstallCorePlugin) { return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err) } + var archError repo.ErrArcNotFound + if errors.As(err, &archError) { + return response.Error(http.StatusNotFound, archError.Error(), nil) + } return response.Error(http.StatusInternalServerError, "Failed to install plugin", err) } diff --git a/pkg/plugins/repo/models.go b/pkg/plugins/repo/models.go index 080e348108e..35f3153022d 100644 --- a/pkg/plugins/repo/models.go +++ b/pkg/plugins/repo/models.go @@ -53,6 +53,15 @@ func (e Response4xxError) Error() string { return fmt.Sprintf("%d", e.StatusCode) } +type ErrArcNotFound struct { + PluginID string + SystemInfo string +} + +func (e ErrArcNotFound) Error() string { + return fmt.Sprintf("%s is not compatible with your system architecture: %s", e.PluginID, e.SystemInfo) +} + type ErrVersionUnsupported struct { PluginID string RequestedVersion string diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index 6cd7dec3739..946feb70602 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -115,10 +115,9 @@ func (m *Manager) selectVersion(plugin *Plugin, version string, compatOpts Compa var ver Version latestForArch := latestSupportedVersion(plugin, compatOpts) if latestForArch == nil { - return nil, ErrVersionUnsupported{ - PluginID: plugin.ID, - RequestedVersion: version, - SystemInfo: compatOpts.String(), + return nil, ErrArcNotFound{ + PluginID: plugin.ID, + SystemInfo: compatOpts.OSAndArch(), } } diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index f99b1fa7d41..73768f9753b 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -103,7 +103,10 @@ export async function getLocalPlugins(): Promise { export async function installPlugin(id: string) { // This will install the latest compatible version based on the logic // on the backend. - return await getBackendSrv().post(`${API_ROOT}/${id}/install`); + return await getBackendSrv().post(`${API_ROOT}/${id}/install`, undefined, { + // Error is displayed in the page + showErrorAlert: false, + }); } export async function uninstallPlugin(id: string) { diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index c79e754c0a9..1169c4afbc7 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { AppEvents } from '@grafana/data'; @@ -9,7 +9,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { removePluginFromNavTree } from 'app/core/reducers/navBarTree'; import { useDispatch } from 'app/types'; -import { useInstallStatus, useUninstallStatus, useInstall, useUninstall } from '../../state/hooks'; +import { useInstallStatus, useUninstallStatus, useInstall, useUninstall, useUnsetInstall } from '../../state/hooks'; import { trackPluginInstalled, trackPluginUninstalled } from '../../tracking'; import { CatalogPlugin, PluginStatus, PluginTabIds, Version } from '../../types'; @@ -33,6 +33,7 @@ export function InstallControlsButton({ const { isUninstalling, error: errorUninstalling } = useUninstallStatus(); const install = useInstall(); const uninstall = useUninstall(); + const unsetInstall = useUnsetInstall(); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); const showConfirmModal = () => setIsConfirmModalVisible(true); const hideConfirmModal = () => setIsConfirmModalVisible(false); @@ -43,10 +44,18 @@ export function InstallControlsButton({ path: location.pathname, }; + useEffect(() => { + return () => { + // Remove possible installation errors + unsetInstall(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const onInstall = async () => { trackPluginInstalled(trackingProps); - await install(plugin.id, latestCompatibleVersion?.version); - if (!errorInstalling) { + const result = await install(plugin.id, latestCompatibleVersion?.version); + if (!errorInstalling && !('error' in result)) { appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]); if (plugin.type === 'app') { setNeedReload(true); @@ -115,7 +124,7 @@ export function InstallControlsButton({ } return ( - ); diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx index 192d450a9fd..53393d86d5f 100644 --- a/public/app/features/plugins/admin/components/PluginSubtitle.tsx +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -2,11 +2,11 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Alert, useStyles2 } from '@grafana/ui'; import { InstallControlsWarning } from '../components/InstallControls'; import { getLatestCompatibleVersion, hasInstallControlWarning } from '../helpers'; -import { useIsRemotePluginsAvailable } from '../state/hooks'; +import { useInstallStatus, useIsRemotePluginsAvailable } from '../state/hooks'; import { CatalogPlugin, PluginStatus } from '../types'; interface Props { @@ -16,6 +16,7 @@ interface Props { export const PluginSubtitle = ({ plugin }: Props) => { const isRemotePluginsAvailable = useIsRemotePluginsAvailable(); const styles = useStyles2(getStyles); + const { error: errorInstalling } = useInstallStatus(); if (!plugin) { return null; } @@ -28,6 +29,11 @@ export const PluginSubtitle = ({ plugin }: Props) => { return (
+ {errorInstalling && ( + + {typeof errorInstalling === 'string' ? errorInstalling : errorInstalling.error} + + )} {plugin?.description &&
{plugin?.description}
} {plugin?.details?.links && plugin.details.links.length > 0 && ( diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index 9eb4d9003aa..3b604ee5d55 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -87,12 +87,17 @@ export const install = createAsyncThunk( return { id, changes } as Update; } catch (e) { console.error(e); + if (isFetchError(e)) { + return thunkApi.rejectWithValue(e.data); + } return thunkApi.rejectWithValue('Unknown error.'); } } ); +export const unsetInstall = createAsyncThunk(`${STATE_PREFIX}/install`, async () => ({})); + export const uninstall = createAsyncThunk(`${STATE_PREFIX}/uninstall`, async (id: string, thunkApi) => { try { await uninstallPlugin(id); diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index 5d2277b1604..98fd4a43972 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'app/types'; import { sortPlugins, Sorters } from '../helpers'; import { CatalogPlugin, PluginListDisplayMode } from '../types'; -import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal } from './actions'; +import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions'; import { setDisplayMode } from './reducer'; import { find, @@ -74,6 +74,12 @@ export const useInstall = () => { return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating })); }; +export const useUnsetInstall = () => { + const dispatch = useDispatch(); + + return () => dispatch(unsetInstall()); +}; + export const useUninstall = () => { const dispatch = useDispatch();