mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: display enterprise plugins in the plugins catalog (#36599)
* displaying enterprise plugins in the list. * added place holder for tests and removed unused code. * added test for the browse page. * added empty test file. * added some more tests.
This commit is contained in:
@@ -4,13 +4,13 @@ import { css } from '@emotion/css';
|
||||
import { Card } from '../components/Card';
|
||||
import { Grid } from '../components/Grid';
|
||||
|
||||
import { Plugin } from '../types';
|
||||
import { Plugin, LocalPlugin } from '../types';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { isLocalPlugin } from '../guards';
|
||||
import { PluginLogo } from './PluginLogo';
|
||||
|
||||
interface Props {
|
||||
plugins: Plugin[];
|
||||
plugins: Array<Plugin | LocalPlugin>;
|
||||
}
|
||||
|
||||
export const PluginList = ({ plugins }: Props) => {
|
||||
@@ -19,11 +19,13 @@ export const PluginList = ({ plugins }: Props) => {
|
||||
return (
|
||||
<Grid>
|
||||
{plugins.map((plugin) => {
|
||||
const { name, orgName, typeCode } = plugin;
|
||||
const id = getPluginId(plugin);
|
||||
const { name } = plugin;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${orgName}-${name}-${typeCode}`}
|
||||
href={`/plugins/${getPluginId(plugin)}`}
|
||||
key={`${id}`}
|
||||
href={`/plugins/${id}`}
|
||||
image={
|
||||
<PluginLogo
|
||||
plugin={plugin}
|
||||
@@ -35,7 +37,7 @@ export const PluginList = ({ plugins }: Props) => {
|
||||
text={
|
||||
<>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.orgName}>{orgName}</div>
|
||||
<div className={styles.orgName}>{getOrgName(plugin)}</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -45,13 +47,20 @@ export const PluginList = ({ plugins }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function getPluginId(plugin: Plugin): string {
|
||||
function getPluginId(plugin: Plugin | LocalPlugin): string {
|
||||
if (isLocalPlugin(plugin)) {
|
||||
return plugin.id;
|
||||
}
|
||||
return plugin.slug;
|
||||
}
|
||||
|
||||
function getOrgName(plugin: Plugin | LocalPlugin): string | undefined {
|
||||
if (isLocalPlugin(plugin)) {
|
||||
return plugin.info?.author?.name;
|
||||
}
|
||||
return plugin.orgName;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
name: css`
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Plugin, LocalPlugin } from '../types';
|
||||
import { api } from '../api';
|
||||
|
||||
type PluginsState = {
|
||||
isLoading: boolean;
|
||||
items: Plugin[];
|
||||
installedPlugins: any[];
|
||||
};
|
||||
|
||||
export const usePlugins = () => {
|
||||
const [state, setState] = useState<PluginsState>({ isLoading: true, items: [], installedPlugins: [] });
|
||||
const result = useAsync(async () => {
|
||||
const items = await api.getRemotePlugins();
|
||||
const filteredPlugins = items.filter((plugin) => {
|
||||
const isNotRenderer = plugin.typeCode !== 'renderer';
|
||||
const isSigned = Boolean(plugin.versionSignatureType);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginData = async () => {
|
||||
const items = await api.getRemotePlugins();
|
||||
const filteredPlugins = items.filter((plugin) => {
|
||||
const isNotRenderer = plugin.typeCode !== 'renderer';
|
||||
const isSigned = Boolean(plugin.versionSignatureType);
|
||||
const isNotEnterprise = plugin.status !== 'enterprise';
|
||||
return isNotRenderer && isSigned;
|
||||
});
|
||||
|
||||
return isNotRenderer && isSigned && isNotEnterprise;
|
||||
});
|
||||
const installedPlugins = await api.getInstalledPlugins();
|
||||
|
||||
const installedPlugins = await api.getInstalledPlugins();
|
||||
|
||||
setState((state) => ({ ...state, items: filteredPlugins, installedPlugins, isLoading: false }));
|
||||
};
|
||||
|
||||
fetchPluginData();
|
||||
return { items: filteredPlugins, installedPlugins };
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
return result;
|
||||
};
|
||||
|
||||
type FilteredPluginsState = {
|
||||
isLoading: boolean;
|
||||
items: Plugin[];
|
||||
items: Array<Plugin | LocalPlugin>;
|
||||
};
|
||||
|
||||
export const usePluginsByFilter = (searchBy: string, filterBy: string): FilteredPluginsState => {
|
||||
const plugins = usePlugins();
|
||||
const { loading, value } = usePlugins();
|
||||
const all = useMemo(() => {
|
||||
const combined: Plugin[] = [];
|
||||
Array.prototype.push.apply(combined, plugins.items);
|
||||
Array.prototype.push.apply(combined, plugins.installedPlugins);
|
||||
Array.prototype.push.apply(combined, value?.items ?? []);
|
||||
Array.prototype.push.apply(combined, value?.installedPlugins ?? []);
|
||||
|
||||
const bySlug = combined.reduce((unique: Record<string, Plugin>, plugin) => {
|
||||
unique[plugin.slug] = plugin;
|
||||
@@ -52,17 +39,17 @@ export const usePluginsByFilter = (searchBy: string, filterBy: string): Filtered
|
||||
}, {});
|
||||
|
||||
return Object.values(bySlug);
|
||||
}, [plugins.items, plugins.installedPlugins]);
|
||||
}, [value?.items, value?.installedPlugins]);
|
||||
|
||||
if (filterBy === 'installed') {
|
||||
return {
|
||||
isLoading: plugins.isLoading,
|
||||
items: applySearchFilter(searchBy, plugins.installedPlugins ?? []),
|
||||
isLoading: loading,
|
||||
items: applySearchFilter(searchBy, value?.installedPlugins ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: plugins.isLoading,
|
||||
isLoading: loading,
|
||||
items: applySearchFilter(searchBy, all),
|
||||
};
|
||||
};
|
||||
@@ -95,17 +82,12 @@ type PluginState = {
|
||||
};
|
||||
|
||||
export const usePlugin = (slug: string): PluginState => {
|
||||
const [state, setState] = useState<PluginState>({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginData = async () => {
|
||||
const plugin = await api.getPlugin(slug);
|
||||
setState({ ...plugin, isLoading: false });
|
||||
};
|
||||
fetchPluginData();
|
||||
const { loading, value } = useAsync(async () => {
|
||||
return await api.getPlugin(slug);
|
||||
}, [slug]);
|
||||
|
||||
return state;
|
||||
return {
|
||||
isLoading: loading,
|
||||
...value,
|
||||
};
|
||||
};
|
||||
|
||||
161
public/app/features/plugins/admin/pages/Browse.test.tsx
Normal file
161
public/app/features/plugins/admin/pages/Browse.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||
import BrowsePage from './Browse';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { Provider } from 'react-redux';
|
||||
import { LocalPlugin, Plugin } from '../types';
|
||||
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as object),
|
||||
getBackendSrv: () => ({
|
||||
get: (path: string) => {
|
||||
switch (path) {
|
||||
case `${GRAFANA_API_ROOT}/plugins`:
|
||||
return Promise.resolve({ items: remote });
|
||||
case API_ROOT:
|
||||
return Promise.resolve(installed);
|
||||
default:
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
function setup(path = '/plugins'): RenderResult {
|
||||
const store = configureStore();
|
||||
locationService.push(path);
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<BrowsePage />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Browse list of plugins', () => {
|
||||
it('should list installed plugins by default', async () => {
|
||||
const { getByText, queryByText } = setup('/plugins');
|
||||
|
||||
await waitFor(() => getByText('Installed'));
|
||||
|
||||
for (const plugin of installed) {
|
||||
expect(getByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
for (const plugin of remote) {
|
||||
expect(queryByText(plugin.name)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('should list all plugins by when filtering by all', async () => {
|
||||
const plugins = [...installed, ...remote];
|
||||
const { getByText } = setup('/plugins?filterBy=all');
|
||||
|
||||
await waitFor(() => getByText('All'));
|
||||
|
||||
for (const plugin of plugins) {
|
||||
expect(getByText(plugin.name)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('should only list plugins matching search', async () => {
|
||||
const { getByText } = setup('/plugins?filterBy=all&q=zabbix');
|
||||
|
||||
await waitFor(() => getByText('All'));
|
||||
|
||||
expect(getByText('Zabbix')).toBeInTheDocument();
|
||||
expect(getByText('1 result')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list enterprise plugins', async () => {
|
||||
const { getByText } = setup('/plugins?filterBy=all&q=wavefront');
|
||||
|
||||
await waitFor(() => getByText('All'));
|
||||
|
||||
expect(getByText('Wavefront')).toBeInTheDocument();
|
||||
expect(getByText('1 result')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const installed: LocalPlugin[] = [
|
||||
{
|
||||
category: '',
|
||||
defaultNavUrl: '/plugins/alertmanager/',
|
||||
info: {
|
||||
author: {
|
||||
name: 'Prometheus alertmanager',
|
||||
url: 'https://grafana.com',
|
||||
},
|
||||
build: {},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
small: '',
|
||||
large: '',
|
||||
},
|
||||
updated: '',
|
||||
version: '',
|
||||
},
|
||||
enabled: true,
|
||||
hasUpdate: false,
|
||||
id: 'alertmanager',
|
||||
latestVersion: '',
|
||||
name: 'Alert Manager',
|
||||
pinned: false,
|
||||
signature: 'internal',
|
||||
signatureOrg: '',
|
||||
signatureType: '',
|
||||
state: 'alpha',
|
||||
type: 'datasource',
|
||||
dev: false,
|
||||
},
|
||||
];
|
||||
const remote: Plugin[] = [
|
||||
{
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
downloads: 33645089,
|
||||
featured: 180,
|
||||
internal: false,
|
||||
links: [],
|
||||
name: 'Zabbix',
|
||||
orgName: 'Alexander Zobnin',
|
||||
orgSlug: 'alexanderzobnin',
|
||||
packages: {},
|
||||
popularity: 0.2111,
|
||||
signatureType: 'community',
|
||||
slug: 'alexanderzobnin-zabbix-app',
|
||||
status: 'active',
|
||||
typeCode: 'app',
|
||||
updatedAt: '2021-05-18T14:53:01.000Z',
|
||||
version: '4.1.5',
|
||||
versionSignatureType: 'community',
|
||||
readme: '',
|
||||
},
|
||||
{
|
||||
createdAt: '2020-09-01T13:02:57.000Z',
|
||||
description: 'Wavefront Datasource',
|
||||
downloads: 7283,
|
||||
featured: 0,
|
||||
internal: false,
|
||||
links: [],
|
||||
name: 'Wavefront',
|
||||
orgName: 'Grafana Labs',
|
||||
orgSlug: 'grafana',
|
||||
packages: {},
|
||||
popularity: 0.0133,
|
||||
signatureType: 'grafana',
|
||||
slug: 'grafana-wavefront-datasource',
|
||||
status: 'enterprise',
|
||||
typeCode: 'datasource',
|
||||
updatedAt: '2021-06-23T12:45:13.000Z',
|
||||
version: '1.0.7',
|
||||
versionSignatureType: 'grafana',
|
||||
readme: '',
|
||||
},
|
||||
];
|
||||
@@ -39,7 +39,7 @@ export default function Browse(): ReactElement {
|
||||
};
|
||||
|
||||
const onSearch = (q: any) => {
|
||||
history.push({ query: { filterBy: null, q } });
|
||||
history.push({ query: { filterBy: 'all', q } });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
import { cx, css } from '@emotion/css';
|
||||
|
||||
import { dateTimeParse, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, Legend, LinkButton } from '@grafana/ui';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { Card } from '../components/Card';
|
||||
import { Grid } from '../components/Grid';
|
||||
import { PluginList } from '../components/PluginList';
|
||||
import { SearchField } from '../components/SearchField';
|
||||
import { PluginTypeIcon } from '../components/PluginTypeIcon';
|
||||
import { usePlugins } from '../hooks/usePlugins';
|
||||
import { Plugin } from '../types';
|
||||
import { Page as PluginPage } from '../components/Page';
|
||||
import { Loader } from '../components/Loader';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
export default function Discover(): JSX.Element | null {
|
||||
const { items, isLoading } = usePlugins();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onSearch = (q: string) => {
|
||||
locationService.push({
|
||||
pathname: '/plugins/browse',
|
||||
search: `?q=${q}`,
|
||||
});
|
||||
};
|
||||
|
||||
const featuredPlugins = items.filter((_) => _.featured > 0);
|
||||
featuredPlugins.sort((a: Plugin, b: Plugin) => {
|
||||
return b.featured - a.featured;
|
||||
});
|
||||
|
||||
const recentlyAdded = items.filter((_) => true);
|
||||
recentlyAdded.sort((a: Plugin, b: Plugin) => {
|
||||
const at = dateTimeParse(a.createdAt);
|
||||
const bt = dateTimeParse(b.createdAt);
|
||||
return bt.valueOf() - at.valueOf();
|
||||
});
|
||||
|
||||
const mostPopular = items.filter((_) => true);
|
||||
mostPopular.sort((a: Plugin, b: Plugin) => {
|
||||
return b.popularity - a.popularity;
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Page>
|
||||
<Page.Contents>
|
||||
<Loader />
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Page.Contents>
|
||||
<PluginPage>
|
||||
<SearchField onSearch={onSearch} />
|
||||
{/* Featured */}
|
||||
<Legend className={styles.legend}>Featured</Legend>
|
||||
<PluginList plugins={featuredPlugins.slice(0, 5)} />
|
||||
|
||||
{/* Most popular */}
|
||||
<div className={styles.legendContainer}>
|
||||
<Legend className={styles.legend}>Most popular</Legend>
|
||||
<LinkButton href={'/plugins/browse?sortBy=popularity'}>See more</LinkButton>
|
||||
</div>
|
||||
<PluginList plugins={mostPopular.slice(0, 5)} />
|
||||
|
||||
{/* Recently added */}
|
||||
<div className={styles.legendContainer}>
|
||||
<Legend className={styles.legend}>Recently added</Legend>
|
||||
<LinkButton href={'/plugins/browse?sortBy=published'}>See more</LinkButton>
|
||||
</div>
|
||||
<PluginList plugins={recentlyAdded.slice(0, 5)} />
|
||||
|
||||
{/* Browse by type */}
|
||||
<Legend className={cx(styles.legend)}>Browse by type</Legend>
|
||||
<Grid>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={'/plugins/browse?filterBy=panel'}
|
||||
image={<PluginTypeIcon typeCode="panel" size={18} />}
|
||||
text={<span className={styles.typeLegend}> Panels</span>}
|
||||
/>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={'/plugins/browse?filterBy=datasource'}
|
||||
image={<PluginTypeIcon typeCode="datasource" size={18} />}
|
||||
text={<span className={styles.typeLegend}> Data sources</span>}
|
||||
/>
|
||||
<Card
|
||||
layout="horizontal"
|
||||
href={'/plugins/browse?filterBy=app'}
|
||||
image={<PluginTypeIcon typeCode="app" size={18} />}
|
||||
text={<span className={styles.typeLegend}> Apps</span>}
|
||||
/>
|
||||
</Grid>
|
||||
</PluginPage>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
legend: css`
|
||||
margin-top: ${theme.spacing(4)};
|
||||
`,
|
||||
legendContainer: css`
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
typeLegend: css`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { PluginList } from '../components/PluginList';
|
||||
import { usePlugins } from '../hooks/usePlugins';
|
||||
import { Page as PluginPage } from '../components/Page';
|
||||
import { Loader } from '../components/Loader';
|
||||
import { CatalogTab, getCatalogNavModel } from './nav';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
export default function Library(): JSX.Element | null {
|
||||
const { isLoading, items, installedPlugins } = usePlugins();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const filteredPlugins = items.filter((plugin) => !!installedPlugins.find((_) => _.id === plugin.slug));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Page navModel={getCatalogNavModel(CatalogTab.Library, '/plugins')}>
|
||||
<Page.Contents>
|
||||
<Loader />
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navModel={getCatalogNavModel(CatalogTab.Library, '/plugins')}>
|
||||
<Page.Contents>
|
||||
<PluginPage>
|
||||
<h1 className={styles.header}>Library</h1>
|
||||
{filteredPlugins.length > 0 ? (
|
||||
<PluginList plugins={filteredPlugins} />
|
||||
) : (
|
||||
<p>
|
||||
You haven't installed any plugins. Browse the{' '}
|
||||
<a className={styles.link} href={'/plugins/browse?sortBy=popularity'}>
|
||||
catalog
|
||||
</a>{' '}
|
||||
for plugins to install.
|
||||
</p>
|
||||
)}
|
||||
</PluginPage>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
header: css`
|
||||
margin-bottom: ${theme.spacing(3)};
|
||||
margin-top: ${theme.spacing(3)};
|
||||
`,
|
||||
link: css`
|
||||
text-decoration: underline;
|
||||
`,
|
||||
};
|
||||
};
|
||||
132
public/app/features/plugins/admin/pages/PluginDetails.test.tsx
Normal file
132
public/app/features/plugins/admin/pages/PluginDetails.test.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||
import PluginDetailsPage from './PluginDetails';
|
||||
import { API_ROOT, GRAFANA_API_ROOT } from '../constants';
|
||||
import { LocalPlugin, Plugin } from '../types';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const original = jest.requireActual('@grafana/runtime');
|
||||
|
||||
return {
|
||||
...original,
|
||||
getBackendSrv: () => ({
|
||||
get: (path: string) => {
|
||||
switch (path) {
|
||||
case `${GRAFANA_API_ROOT}/plugins/not-installed/versions`:
|
||||
case `${GRAFANA_API_ROOT}/plugins/enterprise/versions`:
|
||||
return Promise.resolve([]);
|
||||
case API_ROOT:
|
||||
return Promise.resolve([localPlugin()]);
|
||||
case `${GRAFANA_API_ROOT}/plugins/not-installed`:
|
||||
return Promise.resolve(remotePlugin());
|
||||
case `${GRAFANA_API_ROOT}/plugins/enterprise`:
|
||||
return Promise.resolve(remotePlugin({ status: 'enterprise' }));
|
||||
default:
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
}),
|
||||
config: {
|
||||
...original.config,
|
||||
buildInfo: {
|
||||
...original.config.buildInfo,
|
||||
version: 'v7.5.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function setup(pluginId: string): RenderResult {
|
||||
const props = getRouteComponentProps({ match: { params: { pluginId }, isExact: true, url: '', path: '' } });
|
||||
return render(<PluginDetailsPage {...props} />);
|
||||
}
|
||||
|
||||
describe('Plugin details page', () => {
|
||||
it('should display install button for uninstalled plugins', async () => {
|
||||
const { getByText } = setup('not-installed');
|
||||
|
||||
const expected = 'Install';
|
||||
|
||||
await waitFor(() => getByText(expected));
|
||||
expect(getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display install button for enterprise plugins', async () => {
|
||||
const { getByText } = setup('enterprise');
|
||||
|
||||
const expected = "Marketplace doesn't support installing Enterprise plugins yet. Stay tuned!";
|
||||
|
||||
await waitFor(() => getByText(expected));
|
||||
expect(getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function remotePlugin(plugin: Partial<Plugin> = {}): Plugin {
|
||||
return {
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
downloads: 33645089,
|
||||
featured: 180,
|
||||
internal: false,
|
||||
links: [],
|
||||
name: 'Zabbix',
|
||||
orgName: 'Alexander Zobnin',
|
||||
orgSlug: 'alexanderzobnin',
|
||||
packages: {},
|
||||
popularity: 0.2111,
|
||||
signatureType: 'community',
|
||||
slug: 'alexanderzobnin-zabbix-app',
|
||||
status: 'active',
|
||||
typeCode: 'app',
|
||||
updatedAt: '2021-05-18T14:53:01.000Z',
|
||||
version: '4.1.5',
|
||||
versionSignatureType: 'community',
|
||||
readme: '',
|
||||
json: {
|
||||
dependencies: {
|
||||
grafanaDependency: '>=7.3.0',
|
||||
grafanaVersion: '7.3',
|
||||
},
|
||||
info: {
|
||||
links: [],
|
||||
},
|
||||
},
|
||||
...plugin,
|
||||
};
|
||||
}
|
||||
|
||||
function localPlugin(plugin: Partial<LocalPlugin> = {}): LocalPlugin {
|
||||
return {
|
||||
category: '',
|
||||
defaultNavUrl: '/plugins/alertmanager/',
|
||||
info: {
|
||||
author: {
|
||||
name: 'Prometheus alertmanager',
|
||||
url: 'https://grafana.com',
|
||||
},
|
||||
build: {},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
small: '',
|
||||
large: '',
|
||||
},
|
||||
updated: '',
|
||||
version: '',
|
||||
},
|
||||
enabled: true,
|
||||
hasUpdate: false,
|
||||
id: 'alertmanager',
|
||||
latestVersion: '',
|
||||
name: 'Alert Manager',
|
||||
pinned: false,
|
||||
signature: 'internal',
|
||||
signatureOrg: '',
|
||||
signatureType: '',
|
||||
state: 'alpha',
|
||||
type: 'datasource',
|
||||
dev: false,
|
||||
...plugin,
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { VersionList } from '../components/VersionList';
|
||||
import { InstallControls } from '../components/InstallControls';
|
||||
@@ -12,16 +11,19 @@ import { Page as PluginPage } from '../components/Page';
|
||||
import { Loader } from '../components/Loader';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { PluginLogo } from '../components/PluginLogo';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
export default function PluginDetails(): JSX.Element | null {
|
||||
const { pluginId } = useParams<{ pluginId: string }>();
|
||||
type PluginDetailsProps = GrafanaRouteComponentProps<{ pluginId?: string }>;
|
||||
|
||||
export default function PluginDetails({ match }: PluginDetailsProps): JSX.Element | null {
|
||||
const { pluginId } = match.params;
|
||||
|
||||
const [tabs, setTabs] = useState([
|
||||
{ label: 'Overview', active: true },
|
||||
{ label: 'Version history', active: false },
|
||||
]);
|
||||
|
||||
const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId);
|
||||
const { isLoading, local, remote, remoteVersions } = usePlugin(pluginId!);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const description = remote?.description ?? local?.info?.description;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { NavModel, NavModelItem } from '@grafana/data';
|
||||
|
||||
export enum CatalogTab {
|
||||
Browse = 'browse',
|
||||
Discover = 'discover',
|
||||
Library = 'library',
|
||||
}
|
||||
|
||||
export function getCatalogNavModel(tab: CatalogTab, baseURL: string): NavModel {
|
||||
const pages: NavModelItem[] = [];
|
||||
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/';
|
||||
}
|
||||
|
||||
pages.push({
|
||||
text: 'Browse',
|
||||
icon: 'icon-gf icon-gf-apps',
|
||||
url: `${baseURL}${CatalogTab.Browse}`,
|
||||
id: CatalogTab.Browse,
|
||||
});
|
||||
|
||||
pages.push({
|
||||
text: 'Library',
|
||||
icon: 'icon-gf icon-gf-apps',
|
||||
url: `${baseURL}${CatalogTab.Library}`,
|
||||
id: CatalogTab.Library,
|
||||
});
|
||||
|
||||
const node: NavModelItem = {
|
||||
text: 'Catalog',
|
||||
icon: 'cog',
|
||||
subTitle: 'Manage plugin installations',
|
||||
breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
|
||||
children: setActivePage(tab, pages, CatalogTab.Browse),
|
||||
};
|
||||
|
||||
return {
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
}
|
||||
|
||||
function setActivePage(pageId: CatalogTab, pages: NavModelItem[], defaultPageId: CatalogTab): NavModelItem[] {
|
||||
let found = false;
|
||||
const selected = pageId || defaultPageId;
|
||||
const changed = pages.map((p) => {
|
||||
const active = !found && selected === p.id;
|
||||
if (active) {
|
||||
found = true;
|
||||
}
|
||||
return { ...p, active };
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
changed[0].active = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export interface Plugin {
|
||||
rel: string;
|
||||
href: string;
|
||||
}>;
|
||||
json: {
|
||||
json?: {
|
||||
dependencies: {
|
||||
grafanaDependency: string;
|
||||
grafanaVersion: string;
|
||||
|
||||
Reference in New Issue
Block a user