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 <balogh.levente.hu@gmail.com>

* tests(plugindetails): fix assertions and naming of tests

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* 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 <balogh.levente.hu@gmail.com>
This commit is contained in:
Jack Westbrook 2021-08-26 17:15:43 +02:00 committed by GitHub
parent 568549e810
commit 2cb141d4ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 392 additions and 260 deletions

View File

@ -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<any>;
}
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 (
<HorizontalGroup height="auto" align="center">
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
<LinkButton
href={`${getExternalManageLink(plugin)}?utm_source=grafana_catalog_learn_more`}
target="_blank"
rel="noopener noreferrer"
size="sm"
fill="text"
icon="external-link-alt"
>
Learn more
</LinkButton>
</HorizontalGroup>
);
}
if (isDevelopmentBuild) {
return (
<div className={styles.message}>This is a development build of the plugin and can&#39;t be uninstalled.</div>
);
}
if (!hasPermission && !isExternallyManaged) {
const pluginStatus = isInstalled ? 'uninstall' : hasUpdate ? 'update' : 'install';
const message = `You do not have permission to ${pluginStatus} this plugin.`;
return <div className={styles.message}>{message}</div>;
}
if (isInstalled) {
return (
<HorizontalGroup height="auto">
{hasUpdate &&
(isExternallyManaged ? (
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
{'Update via grafana.com'}
</LinkButton>
) : (
<Button disabled={isInflight || !hasPermission} onClick={onUpdate}>
{isInflight ? 'Updating' : 'Update'}
</Button>
))}
{isExternallyManaged ? (
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
{'Uninstall via grafana.com'}
</LinkButton>
) : (
<>
<Button variant="destructive" disabled={isInflight || !hasPermission} onClick={onUninstall}>
{isInflight && !hasUpdate ? 'Uninstalling' : 'Uninstall'}
</Button>
{hasInstalledPanel && (
<div className={cx(styles.message, styles.messageMargin)}>
Please refresh your browser window before using this plugin.
</div>
)}
</>
)}
</HorizontalGroup>
);
}
if (unsupportedGrafanaVersion) {
return (
<div className={styles.message}>
<Icon name="exclamation-triangle" />
&nbsp;This plugin doesn&#39;t support your version of Grafana.
</div>
);
}
return (
<HorizontalGroup height="auto">
{isExternallyManaged ? (
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
{'Install via grafana.com'}
</LinkButton>
) : (
<>
<Button disabled={isInflight || !hasPermission} onClick={onInstall}>
{isInflight ? 'Installing' : 'Install'}
</Button>
</>
)}
</HorizontalGroup>
);
};
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()};
`,
};
};

View File

@ -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 (
<HorizontalGroup height="auto">
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
Update via grafana.com
</LinkButton>
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
Uninstall via grafana.com
</LinkButton>
</HorizontalGroup>
);
}
if (pluginStatus === PluginStatus.UNINSTALL) {
return (
<LinkButton variant="destructive" href={externalManageLink} target="_blank" rel="noopener noreferrer">
Uninstall via grafana.com
</LinkButton>
);
}
return (
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
Install via grafana.com
</LinkButton>
);
}

View File

@ -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<any>;
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 (
<HorizontalGroup height="auto">
<Button variant="destructive" disabled={isInProgress} onClick={onUninstall}>
{uninstallBtnText}
</Button>
{hasInstalledPanel && (
<div className={styles.message}>Please refresh your browser window before using this plugin.</div>
)}
</HorizontalGroup>
);
}
if (pluginStatus === PluginStatus.UPDATE) {
return (
<HorizontalGroup height="auto">
<Button disabled={isInProgress} onClick={onUpdate}>
{updateBtnText}
</Button>
<Button variant="destructive" disabled={isInProgress} onClick={onUninstall}>
{uninstallBtnText}
</Button>
</HorizontalGroup>
);
}
return (
<Button disabled={isInProgress} onClick={onInstall}>
{installBtnText}
</Button>
);
}

View File

@ -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<any>;
}
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 (
<HorizontalGroup height="auto" align="center">
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
<LinkButton
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
target="_blank"
rel="noopener noreferrer"
size="sm"
fill="text"
icon="external-link-alt"
>
Learn more
</LinkButton>
</HorizontalGroup>
);
}
if (plugin.isDev) {
return (
<div className={styles.message}>This is a development build of the plugin and can&#39;t be uninstalled.</div>
);
}
if (!hasPermission && !isExternallyManaged) {
const message = `You do not have permission to ${pluginStatus} this plugin.`;
return <div className={styles.message}>{message}</div>;
}
if (unsupportedGrafanaVersion) {
return (
<div className={styles.message}>
<Icon name="exclamation-triangle" />
&nbsp;This plugin doesn&#39;t support your version of Grafana.
</div>
);
}
if (isExternallyManaged) {
return <ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />;
}
return (
<InstallControlsButton
isInProgress={isInflight}
dispatch={dispatch}
plugin={plugin}
pluginStatus={pluginStatus}
hasInstalledPanel={hasInstalledPanel}
/>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
message: css`
color: ${theme.colors.text.secondary};
`,
};
};

View File

@ -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(<PluginBadges plugin={plugin} />);
@ -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(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
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(<PluginBadges plugin={{ ...plugin, isEnterprise: true }} />);
expect(screen.getByText(/enterprise/i)).toBeVisible();
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();

View File

@ -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}`;

View File

@ -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> = {}): RemotePlugin {
@ -106,7 +213,8 @@ function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
versionSignedByOrg: 'alexanderzobnin',
versionSignedByOrgName: 'Alexander Zobnin',
userId: 0,
readme: '',
readme:
'<h1>Zabbix plugin for Grafana</h1>\n<p>:copyright: 2015-2021 Alexander Zobnin alexanderzobnin@gmail.com</p>\n<p>Licensed under the Apache 2.0 License</p>',
json: {
dependencies: {
grafanaDependency: '>=7.3.0',
@ -122,65 +230,40 @@ function remotePlugin(plugin: Partial<RemotePlugin> = {}): RemotePlugin {
function localPlugin(plugin: Partial<LocalPlugin> = {}): 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> = {}): 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,
};
}

View File

@ -223,3 +223,9 @@ export type PluginsByFilterType = {
};
export type PluginFilter = (plugin: CatalogPlugin, query: string) => boolean;
export enum PluginStatus {
INSTALL = 'INSTALL',
UNINSTALL = 'UNINSTALL',
UPDATE = 'UPDATE',
}