mirror of https://github.com/grafana/grafana
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.pull/36650/head
parent
561ccf419c
commit
2fb385c192
@ -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: '', |
||||
}, |
||||
]; |
||||
@ -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; |
||||
`,
|
||||
}; |
||||
}; |
||||
@ -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, |
||||
}; |
||||
} |
||||
@ -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; |
||||
} |
||||
Loading…
Reference in new issue