From 2cb141d4ecd6ae361af2a09d338dfa39f54978d3 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 26 Aug 2021 17:15:43 +0200 Subject: [PATCH] Plugins: Refactor installation buttons component (#37642) * refactor(catalog): split out installcontrols into multiple components * test(catalog): update tests for plugindetails page * refactor(catalog): rename installcontrols -> index * refactor(catalog): remove redundant curlies Co-authored-by: Levente Balogh * tests(plugindetails): fix assertions and naming of tests Co-authored-by: Levente Balogh * refactor(installcontrols): prefer enum over duplicate union type, rename disabled prop * refactor(installcontrols): use PluginStatus enum for installcontrols pluginStatus * refactor(installcontrols): remove redundant curlies Co-authored-by: Levente Balogh --- .../admin/components/InstallControls.tsx | 186 ---------------- .../ExternallyManagedButton.tsx | 40 ++++ .../InstallControls/InstallControlsButton.tsx | 93 ++++++++ .../components/InstallControls/index.tsx | 99 +++++++++ .../admin/components/PluginBadges.test.tsx | 19 +- public/app/features/plugins/admin/helpers.ts | 2 + .../admin/pages/PluginDetails.test.tsx | 207 ++++++++++++------ public/app/features/plugins/admin/types.ts | 6 + 8 files changed, 392 insertions(+), 260 deletions(-) delete mode 100644 public/app/features/plugins/admin/components/InstallControls.tsx create mode 100644 public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx create mode 100644 public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx create mode 100644 public/app/features/plugins/admin/components/InstallControls/index.tsx diff --git a/public/app/features/plugins/admin/components/InstallControls.tsx b/public/app/features/plugins/admin/components/InstallControls.tsx deleted file mode 100644 index dd42713967a..00000000000 --- a/public/app/features/plugins/admin/components/InstallControls.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React from 'react'; -import { css, cx } from '@emotion/css'; -import { satisfies } from 'semver'; - -import { config } from '@grafana/runtime'; -import { Button, HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui'; -import { AppEvents, GrafanaTheme2 } from '@grafana/data'; - -import appEvents from 'app/core/app_events'; -import { CatalogPluginDetails, ActionTypes } from '../types'; -import { api } from '../api'; -import { isGrafanaAdmin } from '../helpers'; - -interface Props { - plugin: CatalogPluginDetails; - isInflight: boolean; - hasUpdate: boolean; - hasInstalledPanel: boolean; - isInstalled: boolean; - dispatch: React.Dispatch; -} - -export const InstallControls = ({ plugin, isInflight, hasUpdate, isInstalled, hasInstalledPanel, dispatch }: Props) => { - const isExternallyManaged = config.pluginAdminExternalManageEnabled; - const externalManageLink = getExternalManageLink(plugin); - - const styles = useStyles2(getStyles); - - if (!plugin) { - return null; - } - - const onInstall = async () => { - dispatch({ type: ActionTypes.INFLIGHT }); - try { - await api.installPlugin(plugin.id, plugin.version); - appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]); - dispatch({ type: ActionTypes.INSTALLED, payload: plugin.type === 'panel' }); - } catch (error) { - dispatch({ type: ActionTypes.ERROR, payload: { error } }); - } - }; - - const onUninstall = async () => { - dispatch({ type: ActionTypes.INFLIGHT }); - try { - await api.uninstallPlugin(plugin.id); - appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]); - dispatch({ type: ActionTypes.UNINSTALLED }); - } catch (error) { - dispatch({ type: ActionTypes.ERROR, payload: error }); - } - }; - - const onUpdate = async () => { - dispatch({ type: ActionTypes.INFLIGHT }); - try { - await api.installPlugin(plugin.id, plugin.version); - appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]); - dispatch({ type: ActionTypes.UPDATED }); - } catch (error) { - dispatch({ type: ActionTypes.ERROR, payload: error }); - } - }; - - const grafanaDependency = plugin.grafanaDependency; - const unsupportedGrafanaVersion = grafanaDependency - ? !satisfies(config.buildInfo.version, grafanaDependency, { - // needed for when running against master - includePrerelease: true, - }) - : false; - - const isDevelopmentBuild = Boolean(plugin.isDev); - const isEnterprise = plugin.isEnterprise; - const isCore = plugin.isCore; - const hasPermission = isGrafanaAdmin(); - - if (isCore) { - return null; - } - - if (isEnterprise && !config.licenseInfo?.hasValidLicense) { - return ( - - No valid Grafana Enterprise license detected. - - Learn more - - - ); - } - - if (isDevelopmentBuild) { - return ( -
This is a development build of the plugin and can't be uninstalled.
- ); - } - - if (!hasPermission && !isExternallyManaged) { - const pluginStatus = isInstalled ? 'uninstall' : hasUpdate ? 'update' : 'install'; - const message = `You do not have permission to ${pluginStatus} this plugin.`; - return
{message}
; - } - - if (isInstalled) { - return ( - - {hasUpdate && - (isExternallyManaged ? ( - - {'Update via grafana.com'} - - ) : ( - - ))} - - {isExternallyManaged ? ( - - {'Uninstall via grafana.com'} - - ) : ( - <> - - {hasInstalledPanel && ( -
- Please refresh your browser window before using this plugin. -
- )} - - )} -
- ); - } - - if (unsupportedGrafanaVersion) { - return ( -
- -  This plugin doesn't support your version of Grafana. -
- ); - } - - return ( - - {isExternallyManaged ? ( - - {'Install via grafana.com'} - - ) : ( - <> - - - )} - - ); -}; - -function getExternalManageLink(plugin: CatalogPluginDetails): string { - return `https://grafana.com/grafana/plugins/${plugin.id}`; -} - -export const getStyles = (theme: GrafanaTheme2) => { - return { - message: css` - color: ${theme.colors.text.secondary}; - `, - messageMargin: css` - margin-left: ${theme.spacing()}; - `, - }; -}; diff --git a/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx new file mode 100644 index 00000000000..cbc727393b0 --- /dev/null +++ b/public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { HorizontalGroup, LinkButton } from '@grafana/ui'; +import { getExternalManageLink } from '../../helpers'; +import { PluginStatus } from '../../types'; + +type ExternallyManagedButtonProps = { + pluginId: string; + pluginStatus: PluginStatus; +}; + +export function ExternallyManagedButton({ pluginId, pluginStatus }: ExternallyManagedButtonProps) { + const externalManageLink = getExternalManageLink(pluginId); + + if (pluginStatus === PluginStatus.UPDATE) { + return ( + + + Update via grafana.com + + + Uninstall via grafana.com + + + ); + } + + if (pluginStatus === PluginStatus.UNINSTALL) { + return ( + + Uninstall via grafana.com + + ); + } + + return ( + + Install via grafana.com + + ); +} diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx new file mode 100644 index 00000000000..55ed72aa9ff --- /dev/null +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { AppEvents } from '@grafana/data'; +import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; +import { api } from '../../api'; +import { ActionTypes, CatalogPlugin, PluginStatus } from '../../types'; +import { getStyles } from './index'; + +type InstallControlsButtonProps = { + isInProgress: boolean; + hasInstalledPanel: boolean; + dispatch: React.Dispatch; + plugin: CatalogPlugin; + pluginStatus: PluginStatus; +}; + +export function InstallControlsButton({ + isInProgress, + dispatch, + plugin, + pluginStatus, + hasInstalledPanel, +}: InstallControlsButtonProps) { + const uninstallBtnText = isInProgress ? 'Uninstalling' : 'Uninstall'; + const updateBtnText = isInProgress ? 'Updating' : 'Update'; + const installBtnText = isInProgress ? 'Installing' : 'Install'; + const styles = useStyles2(getStyles); + + const onInstall = async () => { + dispatch({ type: ActionTypes.INFLIGHT }); + try { + await api.installPlugin(plugin.id, plugin.version); + appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]); + dispatch({ type: ActionTypes.INSTALLED, payload: plugin.type === 'panel' }); + } catch (error) { + dispatch({ type: ActionTypes.ERROR, payload: { error } }); + } + }; + + const onUninstall = async () => { + dispatch({ type: ActionTypes.INFLIGHT }); + try { + await api.uninstallPlugin(plugin.id); + appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]); + dispatch({ type: ActionTypes.UNINSTALLED }); + } catch (error) { + dispatch({ type: ActionTypes.ERROR, payload: error }); + } + }; + + const onUpdate = async () => { + dispatch({ type: ActionTypes.INFLIGHT }); + try { + await api.installPlugin(plugin.id, plugin.version); + appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]); + dispatch({ type: ActionTypes.UPDATED }); + } catch (error) { + dispatch({ type: ActionTypes.ERROR, payload: error }); + } + }; + + if (pluginStatus === PluginStatus.UNINSTALL) { + return ( + + + {hasInstalledPanel && ( +
Please refresh your browser window before using this plugin.
+ )} +
+ ); + } + + if (pluginStatus === PluginStatus.UPDATE) { + return ( + + + + + ); + } + + return ( + + ); +} diff --git a/public/app/features/plugins/admin/components/InstallControls/index.tsx b/public/app/features/plugins/admin/components/InstallControls/index.tsx new file mode 100644 index 00000000000..adc1e57622e --- /dev/null +++ b/public/app/features/plugins/admin/components/InstallControls/index.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { css } from '@emotion/css'; +import { satisfies } from 'semver'; + +import { config } from '@grafana/runtime'; +import { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; + +import { CatalogPluginDetails, PluginStatus } from '../../types'; +import { isGrafanaAdmin, getExternalManageLink } from '../../helpers'; +import { ExternallyManagedButton } from './ExternallyManagedButton'; +import { InstallControlsButton } from './InstallControlsButton'; + +interface Props { + plugin: CatalogPluginDetails; + isInflight: boolean; + hasUpdate: boolean; + hasInstalledPanel: boolean; + isInstalled: boolean; + dispatch: React.Dispatch; +} + +export const InstallControls = ({ plugin, isInflight, hasUpdate, isInstalled, hasInstalledPanel, dispatch }: Props) => { + const styles = useStyles2(getStyles); + const isExternallyManaged = config.pluginAdminExternalManageEnabled; + const hasPermission = isGrafanaAdmin(); + const grafanaDependency = plugin.grafanaDependency; + const unsupportedGrafanaVersion = grafanaDependency + ? !satisfies(config.buildInfo.version, grafanaDependency, { + // needed for when running against master + includePrerelease: true, + }) + : false; + const pluginStatus = isInstalled ? (hasUpdate ? PluginStatus.UPDATE : PluginStatus.UNINSTALL) : PluginStatus.INSTALL; + + if (plugin.isCore) { + return null; + } + + if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) { + return ( + + No valid Grafana Enterprise license detected. + + Learn more + + + ); + } + + if (plugin.isDev) { + return ( +
This is a development build of the plugin and can't be uninstalled.
+ ); + } + + if (!hasPermission && !isExternallyManaged) { + const message = `You do not have permission to ${pluginStatus} this plugin.`; + return
{message}
; + } + + if (unsupportedGrafanaVersion) { + return ( +
+ +  This plugin doesn't support your version of Grafana. +
+ ); + } + + if (isExternallyManaged) { + return ; + } + + return ( + + ); +}; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + message: css` + color: ${theme.colors.text.secondary}; + `, + }; +}; diff --git a/public/app/features/plugins/admin/components/PluginBadges.test.tsx b/public/app/features/plugins/admin/components/PluginBadges.test.tsx index a249a57f8b2..c801012f569 100644 --- a/public/app/features/plugins/admin/components/PluginBadges.test.tsx +++ b/public/app/features/plugins/admin/components/PluginBadges.test.tsx @@ -3,16 +3,7 @@ import { render, screen } from '@testing-library/react'; import { PluginSignatureStatus } from '@grafana/data'; import { PluginBadges } from './PluginBadges'; import { CatalogPlugin } from '../types'; - -const runtimeMock = jest.requireMock('@grafana/runtime'); - -jest.mock('@grafana/runtime', () => ({ - config: { - licenseInfo: { - hasValidLicense: false, - }, - }, -})); +import { config } from '@grafana/runtime'; describe('PluginBadges', () => { const plugin: CatalogPlugin = { @@ -39,6 +30,10 @@ describe('PluginBadges', () => { isEnterprise: false, }; + afterEach(() => { + jest.clearAllMocks(); + }); + it('renders a plugin signature badge', () => { render(); @@ -53,14 +48,14 @@ describe('PluginBadges', () => { }); it('renders an enterprise badge (when a license is valid)', () => { - runtimeMock.config.licenseInfo.hasValidLicense = true; + config.licenseInfo.hasValidLicense = true; render(); expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument(); }); it('renders an enterprise badge with icon and link (when a license is invalid)', () => { - runtimeMock.config.licenseInfo.hasValidLicense = false; + config.licenseInfo.hasValidLicense = false; render(); expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument(); diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 3a4b997dd93..0b9f4bddccb 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -166,3 +166,5 @@ export const matchesKeyword: PluginFilter = (plugin, query) => { return fields.some((f) => f.includes(query.toLowerCase())); }; + +export const getExternalManageLink = (pluginId: string) => `https://grafana.com/grafana/plugins/${pluginId}`; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index e001e3cf114..e2430123e37 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { render, RenderResult, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { config } from '@grafana/runtime'; import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data'; import PluginDetailsPage from './PluginDetails'; import { API_ROOT, GRAFANA_API_ROOT } from '../constants'; @@ -17,12 +19,30 @@ jest.mock('@grafana/runtime', () => { case `${GRAFANA_API_ROOT}/plugins/not-installed/versions`: case `${GRAFANA_API_ROOT}/plugins/enterprise/versions`: return Promise.resolve([]); + case `${GRAFANA_API_ROOT}/plugins/installed/versions`: + return Promise.resolve({ + items: [ + { + version: '1.0.0', + createdAt: '2016-04-06T20:23:41.000Z', + }, + ], + }); case API_ROOT: - return Promise.resolve([localPlugin(), corePlugin()]); + return Promise.resolve([ + localPlugin(), + localPlugin({ id: 'installed', signature: PluginSignatureStatus.valid }), + localPlugin({ id: 'has-update', signature: PluginSignatureStatus.valid }), + localPlugin({ id: 'core', signature: PluginSignatureStatus.internal }), + ]); case `${GRAFANA_API_ROOT}/plugins/core`: - return Promise.resolve(corePlugin()); + return Promise.resolve(localPlugin({ id: 'core', signature: PluginSignatureStatus.internal })); case `${GRAFANA_API_ROOT}/plugins/not-installed`: return Promise.resolve(remotePlugin()); + case `${GRAFANA_API_ROOT}/plugins/has-update`: + return Promise.resolve(remotePlugin({ slug: 'has-update', version: '2.0.0' })); + case `${GRAFANA_API_ROOT}/plugins/installed`: + return Promise.resolve(remotePlugin({ slug: 'installed' })); case `${GRAFANA_API_ROOT}/plugins/enterprise`: return Promise.resolve(remotePlugin({ status: 'enterprise' })); default: @@ -53,18 +73,84 @@ function setup(pluginId: string): RenderResult { } describe('Plugin details page', () => { - it('should display install button for uninstalled plugins', async () => { - const { getByText } = setup('not-installed'); + let dateNow: any; - const expected = 'Install'; - - await waitFor(() => expect(getByText(expected)).toBeInTheDocument()); + beforeAll(() => { + dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00 }); - it('should not display install button for enterprise plugins', async () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + dateNow.mockRestore(); + }); + + it('should display an overview (plugin readme) by default', async () => { + const { queryByText } = setup('not-installed'); + await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument()); + }); + + it('should display version history', async () => { + const { queryByText, getByText, getByRole } = setup('installed'); + await waitFor(() => expect(queryByText(/version history/i)).toBeInTheDocument()); + userEvent.click(getByText(/version history/i)); + expect( + getByRole('columnheader', { + name: /version/i, + }) + ).toBeInTheDocument(); + expect( + getByRole('columnheader', { + name: /last updated/i, + }) + ).toBeInTheDocument(); + expect( + getByRole('cell', { + name: /1\.0\.0/i, + }) + ).toBeInTheDocument(); + expect( + getByRole('cell', { + name: /5 years ago/i, + }) + ).toBeInTheDocument(); + }); + + it("should display install button for a plugin that isn't installed", async () => { + const { queryByRole } = setup('not-installed'); + + await waitFor(() => expect(queryByRole('button', { name: /install/i })).toBeInTheDocument()); + expect(queryByRole('button', { name: /uninstall/i })).not.toBeInTheDocument(); + }); + + it('should display uninstall button for an installed plugin', async () => { + const { queryByRole } = setup('installed'); + await waitFor(() => expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument()); + }); + + it('should display update and uninstall buttons for a plugin with update', async () => { + const { queryByRole } = setup('has-update'); + + await waitFor(() => expect(queryByRole('button', { name: /update/i })).toBeInTheDocument()); + expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument(); + }); + + it('should display install button for enterprise plugins if license is valid', async () => { + config.licenseInfo.hasValidLicense = true; const { queryByRole } = setup('enterprise'); - await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument()); + await waitFor(() => expect(queryByRole('button', { name: /install/i })).toBeInTheDocument()); + }); + + it('should not display install button for enterprise plugins if license is invalid', async () => { + config.licenseInfo.hasValidLicense = false; + const { queryByRole, queryByText } = setup('enterprise'); + + await waitFor(() => expect(queryByRole('button', { name: /install/i })).not.toBeInTheDocument()); + expect(queryByText(/no valid Grafana Enterprise license detected/i)).toBeInTheDocument(); + expect(queryByRole('link', { name: /learn more/i })).toBeInTheDocument(); }); it('should not display install / uninstall buttons for core plugins', async () => { @@ -72,6 +158,27 @@ describe('Plugin details page', () => { await waitFor(() => expect(queryByRole('button', { name: /(un)?install/i })).not.toBeInTheDocument()); }); + + it('should display install link with pluginAdminExternalManageEnabled true', async () => { + config.pluginAdminExternalManageEnabled = true; + const { queryByRole } = setup('not-installed'); + + await waitFor(() => expect(queryByRole('link', { name: /install via grafana.com/i })).toBeInTheDocument()); + }); + + it('should display uninstall link for an installed plugin with pluginAdminExternalManageEnabled true', async () => { + config.pluginAdminExternalManageEnabled = true; + const { queryByRole } = setup('installed'); + await waitFor(() => expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument()); + }); + + it('should display update and uninstall links for a plugin with update and pluginAdminExternalManageEnabled true', async () => { + config.pluginAdminExternalManageEnabled = true; + const { queryByRole } = setup('has-update'); + + await waitFor(() => expect(queryByRole('link', { name: /update via grafana.com/i })).toBeInTheDocument()); + expect(queryByRole('link', { name: /uninstall via grafana.com/i })).toBeInTheDocument(); + }); }); function remotePlugin(plugin: Partial = {}): RemotePlugin { @@ -106,7 +213,8 @@ function remotePlugin(plugin: Partial = {}): RemotePlugin { versionSignedByOrg: 'alexanderzobnin', versionSignedByOrgName: 'Alexander Zobnin', userId: 0, - readme: '', + readme: + '

Zabbix plugin for Grafana

\n

:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com

\n

Licensed under the Apache 2.0 License

', json: { dependencies: { grafanaDependency: '>=7.3.0', @@ -122,65 +230,40 @@ function remotePlugin(plugin: Partial = {}): RemotePlugin { function localPlugin(plugin: Partial = {}): LocalPlugin { return { - category: '', - defaultNavUrl: '/plugins/alertmanager/', + name: 'Akumuli', + type: PluginType.datasource, + id: 'akumuli-datasource', + enabled: true, + pinned: false, info: { author: { - name: 'Prometheus alertmanager', - url: 'https://grafana.com', + name: 'Eugene Lazin', + url: 'https://akumuli.org', + }, + description: 'Datasource plugin for Akumuli time-series database', + links: [ + { + name: 'Project site', + url: 'https://github.com/akumuli/Akumuli', + }, + ], + logos: { + small: 'public/plugins/akumuli-datasource/img/logo.svg.png', + large: 'public/plugins/akumuli-datasource/img/logo.svg.png', }, build: {}, - description: '', - links: [], - logos: { - small: '', - large: '', - }, - updated: '', - version: '', + screenshots: null, + version: '1.3.12', + updated: '2019-12-19', }, - enabled: true, + latestVersion: '1.3.12', hasUpdate: false, - id: 'alertmanager', - latestVersion: '', - name: 'Alert Manager', - pinned: false, - signature: PluginSignatureStatus.internal, - signatureOrg: '', - signatureType: '', - state: 'alpha', - type: PluginType.datasource, - ...plugin, - }; -} - -function corePlugin(plugin: Partial = {}): LocalPlugin { - return { - category: 'sql', - defaultNavUrl: '/plugins/postgres/', - enabled: true, - hasUpdate: false, - id: 'core', - info: { - author: { name: 'Grafana Labs', url: 'https://grafana.com' }, - build: {}, - description: 'Data source for PostgreSQL and compatible databases', - links: [], - logos: { - small: 'public/app/plugins/datasource/postgres/img/postgresql_logo.svg', - large: 'public/app/plugins/datasource/postgres/img/postgresql_logo.svg', - }, - updated: '', - version: '', - }, - latestVersion: '', - name: 'PostgreSQL', - pinned: false, - signature: PluginSignatureStatus.internal, - signatureOrg: '', - signatureType: '', + defaultNavUrl: '/plugins/akumuli-datasource/', + category: '', state: '', - type: PluginType.datasource, + signature: PluginSignatureStatus.valid, + signatureType: 'community', + signatureOrg: 'Grafana Labs', ...plugin, }; } diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 6a4ef3541f3..055fc29bdd7 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -223,3 +223,9 @@ export type PluginsByFilterType = { }; export type PluginFilter = (plugin: CatalogPlugin, query: string) => boolean; + +export enum PluginStatus { + INSTALL = 'INSTALL', + UNINSTALL = 'UNINSTALL', + UPDATE = 'UPDATE', +}